Support captions

This adds Mmark support for captions under quotes block or fenced code
blocks. This can be used by adding: `Caption: <text>` under the block.
All text up to the next empty line is read.

This adds ast.Caption, which is a noop in the html/renderer because the
elements need to walk this node outside of the main ast. The node can
only hold span elements (p.inline()).

Signed-off-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
Miek Gieben 2018-07-31 10:54:01 +01:00
parent 4a5b716e6a
commit 77587eb931
7 changed files with 120 additions and 11 deletions

View File

@ -276,11 +276,13 @@ implements the following extensions:
```
Will convert into `<h1 id="id3" class="myclass" fontsize="tiny">Header 1</h1>`.
* **Mmark Special Heading**. A heading stating with `#.`, this makes it a special header. This can
* **Mmark special heading**. A heading stating with `#.`, this makes it a special header. This can
be used to typeset Abstract or Prefaces.
* **Mmark document divisions**, allow front-, main- or backmatter.
* **Mmark captions**, allow captions under code blocks and block quotes, by using `Caption: <text>`
## Todo
* port https://github.com/russross/blackfriday/issues/348

View File

@ -146,6 +146,8 @@ type DocumentMatter struct {
// BlockQuote represents markdown block quote node
type BlockQuote struct {
Container
Caption Node // If enabled (MmarkCaption) holds the caption.
}
// Aside represents an markdown aside node.
@ -261,7 +263,7 @@ type CodeBlock struct {
FenceLength int
FenceOffset int
Caption []byte // If enabled (MmarkCaption) holds the caption.
Caption Node // If enabled (MmarkCaption) holds the caption.
}
// Softbreak represents markdown softbreak node
@ -313,6 +315,11 @@ type TableRow struct {
Container
}
// Caption represents a figure, code or quote caption
type Caption struct {
Container
}
func removeNodeFromArray(a []Node, node Node) []Node {
n := len(a)
for i := 0; i < n; i++ {

View File

@ -761,20 +761,58 @@ func (r *Renderer) listItem(w io.Writer, listItem *ast.ListItem, entering bool)
}
func (r *Renderer) codeBlock(w io.Writer, codeBlock *ast.CodeBlock) {
r.cr(w)
var attrs []string
// if a caption has been given, wrap in <figure> and add <figcaption>.
if codeBlock.Caption != nil {
r.outs(w, "<figure>")
}
attrs = appendLanguageAttr(attrs, codeBlock.Info)
attrs = append(attrs, blockAttrs(codeBlock)...)
r.cr(w)
r.outs(w, "<pre>")
r.outTag(w, "<code", attrs)
EscapeHTML(w, codeBlock.Literal)
r.outs(w, "</code>")
r.outs(w, "</pre>")
if codeBlock.Caption != nil {
r.outs(w, "<figcaption>")
ast.WalkFunc(codeBlock.Caption, func(node ast.Node, entering bool) ast.WalkStatus {
return r.RenderNode(w, node, entering)
})
r.outs(w, "</figcaption></figure>")
}
if !isListItem(codeBlock.Parent) {
r.cr(w)
}
}
func (r *Renderer) blockQuote(w io.Writer, quote *ast.BlockQuote, entering bool) {
if entering {
// if a caption has been given, wrap in <figure> and add <figcaption>.
if quote.Caption != nil {
r.outs(w, "<figure>")
}
tag := tagWithAttributes("<blockquote", blockAttrs(quote))
r.cr(w)
r.outs(w, tag)
return
}
r.outs(w, "</blockquote>")
if quote.Caption != nil {
r.outs(w, "<figcaption>")
ast.WalkFunc(quote.Caption, func(node ast.Node, entering bool) ast.WalkStatus {
return r.RenderNode(w, node, entering)
})
r.outs(w, "</figcaption></figure>")
}
r.cr(w)
}
func (r *Renderer) tableCell(w io.Writer, tableCell *ast.TableCell, entering bool) {
if !entering {
r.outOneOf(w, tableCell.IsHeader, "</th>", "</td>")
@ -853,8 +891,7 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal
case *ast.Del:
r.outOneOf(w, entering, "<del>", "</del>")
case *ast.BlockQuote:
tag := tagWithAttributes("<blockquote", blockAttrs(node))
r.outOneOfCr(w, entering, tag, "</blockquote>")
r.blockQuote(w, node, entering)
case *ast.Aside:
tag := tagWithAttributes("<aside", blockAttrs(node))
r.outOneOfCr(w, entering, tag, "</aside>")
@ -871,6 +908,8 @@ func (r *Renderer) RenderNode(w io.Writer, node ast.Node, entering bool) ast.Wal
r.codeBlock(w, node)
case *ast.Document:
// do nothing
case *ast.Caption:
// do nothing
case *ast.Paragraph:
r.paragraph(w, node, entering)
case *ast.HTMLSpan:

View File

@ -22,7 +22,7 @@ func TestMmark(t *testing.T) {
t.Fatalf("odd test tuples: %d", len(testdata))
}
ext := parser.CommonExtensions | parser.Attributes | parser.OrderedListStart | parser.MmarkSpecialHeading | parser.MmarkMatters
ext := parser.CommonExtensions | parser.Attributes | parser.OrderedListStart | parser.MmarkSpecialHeading | parser.MmarkMatters | parser.MmarkCaptions
for i := 0; i < len(testdata); i += 2 {
p := parser.NewWithExtensions(ext)
@ -31,8 +31,14 @@ func TestMmark(t *testing.T) {
got := ToHTML([]byte(input), p, nil)
// make whitespace more visible
got = bytes.Replace(got, []byte(" "), []byte("_"), -1)
want = bytes.Replace(want, []byte(" "), []byte("_"), -1)
got = bytes.Replace(got, []byte("\n"), []byte("_\n"), -1)
want = bytes.Replace(want, []byte("\n"), []byte("_\n"), -1)
if bytes.Compare(got, want) != 0 {
t.Errorf("want %s, got %s, for input %q", want, got, input)
t.Errorf("want (%d bytes) %s, got (%d bytes) %s, for input %q", len(want), want, len(got), got, input)
}
}
}

View File

@ -897,14 +897,17 @@ func (p *Parser) fencedCodeBlock(data []byte, doRender bool) int {
}
beg = end
}
if p.extensions | MmarkCaptions {
}
if doRender {
codeBlock := &ast.CodeBlock{
IsFenced: true,
}
if p.extensions|MmarkCaptions != 0 {
caption, consumed := p.caption([]byte("Caption: "), data[beg:])
codeBlock.Caption = caption
beg += consumed
}
// TODO: get rid of temp buffer
codeBlock.Content = work.Bytes()
p.addBlock(codeBlock)
@ -1169,7 +1172,8 @@ func (p *Parser) terminateBlockquote(data []byte, beg, end int) bool {
// parse a blockquote fragment
func (p *Parser) quote(data []byte) int {
block := p.addBlock(&ast.BlockQuote{})
quote := &ast.BlockQuote{}
block := p.addBlock(quote)
var raw bytes.Buffer
beg, end := 0, 0
for beg < len(data) {
@ -1200,6 +1204,13 @@ func (p *Parser) quote(data []byte) int {
}
p.block(raw.Bytes())
p.finalize(block)
if p.extensions|MmarkCaptions != 0 {
caption, consumed := p.caption([]byte("Caption: "), data[end:])
quote.Caption = caption
end += consumed
}
return end
}

View File

@ -1,5 +1,25 @@
package parser
import (
"bytes"
"github.com/gomarkdown/markdown/ast"
)
func (p *Parser) caption(startwith, data []byte) (ast.Node, int) {
if !bytes.HasPrefix(data, startwith) {
return nil, 0
}
j := len(startwith)
data = data[j:]
end := p.linesUntilEmpty(data)
node := &ast.Caption{}
p.inline(node, data[:end])
return node, end + j
}
// linesUntilEmpty scans lines up to the first empty line.
func (p *Parser) linesUntilEmpty(data []byte) int {
line, i := 0, 0

24
testdata/mmark.test vendored
View File

@ -16,3 +16,27 @@
<h1>Section in matter</h1>
</section>
<section matter="main"></section>
---
# Test Code Captions
~~~ go
println("hi")
~~~
Caption: This *is* a
caption.
---
<h1>Test Code Captions</h1>
<figure><pre><code class="language-go">println(&quot;hi&quot;)
</code></pre><figcaption>This <em>is</em> a
caption.</figcaption></figure>
---
# Test Quote Captions
> To be, or not to be
Caption: Shakespeare.
---
<h1>Test Quote Captions</h1>
<figure>
<blockquote>
<p>To be, or not to be</p>
</blockquote><figcaption>Shakespeare.</figcaption></figure>