diff --git a/block.go b/block.go index 4a7df43..42bed26 100644 --- a/block.go +++ b/block.go @@ -1380,7 +1380,10 @@ func (p *parser) paragraph(data []byte) int { // line: index of 1st char of current line // i: index of cursor/end of current line var prev, line, i int - + tabSize := TabSizeDefault + if p.flags&TabSizeEight != 0 { + tabSize = TabSizeDouble + } // keep going until we find something to mark the end of the paragraph for i < len(data) { // mark the beginning of the current line @@ -1388,6 +1391,14 @@ func (p *parser) paragraph(data []byte) int { current := data[i:] line = i + // did we find a reference or a footnote? If so, end a paragraph + // preceding it and report that we have consumed up to the end of that + // reference: + if refEnd := isReference(p, current, tabSize); refEnd > 0 { + p.renderParagraph(data[:i]) + return i + refEnd + } + // did we find a blank line marking the end of the paragraph? if n := p.isEmpty(current); n > 0 { // did this blank line followed by a definition list item? diff --git a/html.go b/html.go index 3f3cf32..54f2bea 100644 --- a/html.go +++ b/html.go @@ -589,10 +589,11 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt tagName = "dl" } if entering { - // var start = node.listStart; - // if (start !== null && start !== 1) { - // attrs.push(['start', start.toString()]); - // } + if node.IsFootnotesList { + r.out(w, []byte("\n
\n\n")) + r.out(w, tag("hr", attrs, r.Flags&UseXHTML != 0)) + r.cr(w) + } r.cr(w) if node.Parent.Type == Item && node.Parent.Parent.Tight { r.cr(w) @@ -611,6 +612,9 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt if node.Parent.Type == Document || node.Parent.Type == BlockQuote { r.cr(w) } + if node.IsFootnotesList { + r.out(w, []byte("\n
\n")) + } } case Item: tagName := "li" diff --git a/inline_test.go b/inline_test.go index 4be754c..598e5e6 100644 --- a/inline_test.go +++ b/inline_test.go @@ -922,6 +922,12 @@ what happens here `, + + `This text does not reference a footnote. + +[^footnote]: But it has a footnote! And it gets omitted. +`, + "

This text does not reference a footnote.

\n", } func TestFootnotes(t *testing.T) { diff --git a/markdown.go b/markdown.go index 71b41e5..7fa0e20 100644 --- a/markdown.go +++ b/markdown.go @@ -75,7 +75,7 @@ const ( ListTypeTerm ListItemContainsBlock - ListItemBeginningOfList + ListItemBeginningOfList // TODO: figure out if this is of any use now ListItemEndOfList ) @@ -387,8 +387,7 @@ func Parse(input []byte, opts Options) *Node { p.notes = make([]*reference, 0) } - first := firstPass(p, input) - secondPass(p, first) + p.block(preprocess(p, input)) // Walk the tree and finish up some of unfinished blocks for p.tip != nil { p.finalize(p.tip) @@ -410,9 +409,8 @@ func (p *parser) parseRefsToAST() { return } p.tip = p.doc - finalizeHTMLBlock(p.addBlock(HTMLBlock, []byte(`
`))) - p.addBlock(HorizontalRule, nil) block := p.addBlock(List, nil) + block.IsFootnotesList = true block.ListFlags = ListTypeOrdered flags := ListItemBeginningOfList // Note: this loop is intentionally explicit, not range-form. This is @@ -422,7 +420,7 @@ func (p *parser) parseRefsToAST() { for i := 0; i < len(p.notes); i++ { ref := p.notes[i] block := p.addBlock(Item, nil) - block.ListFlags = ListTypeOrdered + block.ListFlags = flags | ListTypeOrdered block.RefLink = ref.link if ref.hasBlock { flags |= ListItemContainsBlock @@ -435,7 +433,6 @@ func (p *parser) parseRefsToAST() { above := block.Parent finalizeList(block) p.tip = above - finalizeHTMLBlock(p.addBlock(HTMLBlock, []byte("
"))) block.Walk(func(node *Node, entering bool) WalkStatus { if node.Type == Paragraph || node.Type == Header { p.inline(node, node.content) @@ -445,12 +442,11 @@ func (p *parser) parseRefsToAST() { }) } -// first pass: +// preprocess does a preparatory first pass over the input: // - normalize newlines -// - extract references (outside of fenced code blocks) // - expand tabs (outside of fenced code blocks) // - copy everything else -func firstPass(p *parser, input []byte) []byte { +func preprocess(p *parser, input []byte) []byte { var out bytes.Buffer tabSize := TabSizeDefault if p.flags&TabSizeEight != 0 { @@ -479,9 +475,6 @@ func firstPass(p *parser, input []byte) []byte { if end > beg { if end < lastFencedCodeBlockEnd { // Do not expand tabs while inside fenced code blocks. out.Write(input[beg:end]) - } else if refEnd := isReference(p, input[beg:], tabSize); refEnd > 0 { - beg += refEnd - continue } else { expandTabs(&out, input[beg:end], tabSize) } @@ -506,29 +499,6 @@ func firstPass(p *parser, input []byte) []byte { return out.Bytes() } -// second pass: actual rendering -func secondPass(p *parser, input []byte) { - p.block(input) - - if p.flags&Footnotes != 0 && len(p.notes) > 0 { - flags := ListItemBeginningOfList - for i := 0; i < len(p.notes); i++ { - ref := p.notes[i] - if ref.hasBlock { - flags |= ListItemContainsBlock - p.block(ref.title) - } else { - p.inline(nil, ref.title) - } - flags &^= ListItemBeginningOfList | ListItemContainsBlock - } - } - - if p.nesting != 0 { - panic("Nesting level did not end at zero") - } -} - // // Link references // @@ -558,13 +528,50 @@ func secondPass(p *parser, input []byte) { // // are not yet supported. -// References are parsed and stored in this struct. +// reference holds all information necessary for a reference-style links or +// footnotes. +// +// Consider this markdown with reference-style links: +// +// [link][ref] +// +// [ref]: /url/ "tooltip title" +// +// It will be ultimately converted to this HTML: +// +//

link

+// +// And a reference structure will be populated as follows: +// +// p.refs["ref"] = &reference{ +// link: "/url/", +// title: "tooltip title", +// } +// +// Alternatively, reference can contain information about a footnote. Consider +// this markdown: +// +// Text needing a footnote.[^a] +// +// [^a]: This is the note +// +// A reference structure will be populated as follows: +// +// p.refs["a"] = &reference{ +// link: "a", +// title: "This is the note", +// noteID: , +// } +// +// TODO: As you can see, it begs for splitting into two dedicated structures +// for refs and for footnotes. type reference struct { link []byte title []byte noteID int // 0 if not a footnote ref hasBlock bool - text []byte + + text []byte // only gets populated by refOverride feature with Reference.Text } func (r *reference) String() string { @@ -610,7 +617,11 @@ func isReference(p *parser, data []byte, tabSize int) int { return 0 } idEnd := i - + // footnotes can have empty ID, like this: [^], but a reference can not be + // empty like this: []. Break early if it's not a footnote and there's no ID + if noteID == 0 && idOffset == idEnd { + return 0 + } // spacer: colon (space | tab)* newline? (space | tab)* i++ if i >= len(data) || data[i] != ':' { diff --git a/node.go b/node.go index 3cebd11..983ec10 100644 --- a/node.go +++ b/node.go @@ -69,20 +69,21 @@ func (t NodeType) String() string { return nodeTypeNames[t] } -// ListData contains fields relevant to a List node type. +// ListData contains fields relevant to a List and Item node type. type ListData struct { - ListFlags ListType - Tight bool // Skip

s around list item data if true - BulletChar byte // '*', '+' or '-' in bullet lists - Delimiter byte // '.' or ')' after the number in ordered lists - RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering + ListFlags ListType + Tight bool // Skip

s around list item data if true + BulletChar byte // '*', '+' or '-' in bullet lists + Delimiter byte // '.' or ')' after the number in ordered lists + RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering + IsFootnotesList bool // This is a list of footnotes } // LinkData contains fields relevant to a Link node type. type LinkData struct { - Destination []byte - Title []byte - NoteID int + Destination []byte // Destination is what goes into a href + Title []byte // Title is the tooltip thing that goes in a title attribute + NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote } // CodeBlockData contains fields relevant to a CodeBlock node type.