Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic Footnotes implementation #78

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
52 changes: 27 additions & 25 deletions src/markd/node.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,56 @@ module Markd
class Node
# Node Type
enum Type
Document
Paragraph
Text
Strong
Emphasis
Strikethrough
Link
Image
Heading
List
Item
BlockQuote
ThematicBreak
Code
CodeBlock
CustomBlock
CustomInLine
Document
Emphasis
Footnote
FootnoteDefinition
Heading
HTMLBlock
HTMLInline
Image
Item
LineBreak
Link
List
Paragraph
SoftBreak

CustomInLine
CustomBlock
Strikethrough
Strong
Table
TableCell
TableRow
Text
ThematicBreak

def container?
CONTAINER_TYPES.includes?(self)
end
end

CONTAINER_TYPES = {
Type::BlockQuote,
Type::CustomBlock,
Type::CustomInLine,
Type::Document,
Type::Paragraph,
Type::Strong,
Type::Emphasis,
Type::Strikethrough,
Type::Link,
Type::Image,
Type::FootnoteDefinition,
Type::Heading,
Type::List,
Type::Image,
Type::Item,
Type::BlockQuote,
Type::CustomInLine,
Type::CustomBlock,
Type::Link,
Type::List,
Type::Paragraph,
Type::Strikethrough,
Type::Strong,
Type::Table,
Type::TableRow,
Type::TableCell,
Type::TableRow,
}

alias DataValue = String | Int32 | Bool
Expand Down
72 changes: 61 additions & 11 deletions src/markd/parsers/block.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ module Markd::Parser
end

RULES = {
Node::Type::Document => Rule::Document.new,
Node::Type::BlockQuote => Rule::BlockQuote.new,
Node::Type::Heading => Rule::Heading.new,
Node::Type::CodeBlock => Rule::CodeBlock.new,
Node::Type::HTMLBlock => Rule::HTMLBlock.new,
Node::Type::ThematicBreak => Rule::ThematicBreak.new,
Node::Type::List => Rule::List.new,
Node::Type::Item => Rule::Item.new,
Node::Type::Paragraph => Rule::Paragraph.new,
Node::Type::Table => Rule::Table.new,
Node::Type::Document => Rule::Document.new,
Node::Type::BlockQuote => Rule::BlockQuote.new,
Node::Type::Heading => Rule::Heading.new,
Node::Type::CodeBlock => Rule::CodeBlock.new,
Node::Type::HTMLBlock => Rule::HTMLBlock.new,
Node::Type::ThematicBreak => Rule::ThematicBreak.new,
Node::Type::List => Rule::List.new,
Node::Type::Item => Rule::Item.new,
Node::Type::Paragraph => Rule::Paragraph.new,
Node::Type::Table => Rule::Table.new,
Node::Type::FootnoteDefinition => Rule::FootnoteDefinition.new,
}

property! tip : Node?
Expand Down Expand Up @@ -62,6 +63,55 @@ module Markd::Parser
process_inlines
end

if @options.gfm
# Extract all footnotes and footnote definitions
walker = @document.walker
footnotes = {} of String => Node
footnote_definitions = {} of String => Node
while (event = walker.next)
node, entering = event
if node.type.footnote?
footnotes[node.data["title"].to_s] = node
elsif !entering && node.type.footnote_definition?
footnote_definitions[node.data["title"].to_s] = node
end
end

# footnotes without definitions are converted to text
# and removed from our hash
footnotes.each do |title, _node|
if !footnote_definitions.keys.includes? title
_node.type = Node::Type::Text
_node.text = "[^#{title}]"
footnotes.delete title
end
end
# definitions without footnotes are removed
# and popped from our hash
footnote_definitions.each do |title, _node|
if !footnotes.keys.includes? title
_node.unlink
footnote_definitions.delete title
end
end
# Footnote numbers are normalized to 1...n
# Footnotes are always ordered because the important thing is
# appearing in the right order in the document, but now there
# may be holes in the numbering.
# Also, definitions get the matching number in their own
# metadata.
footnotes.each_with_index do |(title, node), index|
node.data["number"] = index + 1
footnote_definitions[title].data["number"] = index + 1
end

# Footnote definitionss are moved to the end of the document
footnotes.each do |(title, _)|
node = footnote_definitions[title]
node.unlink
@document.append_child(node)
end
end
@document
end

Expand Down Expand Up @@ -197,7 +247,7 @@ module Markd::Parser
@inline_lexer.refmap = @refmap
while (event = walker.next)
node, entering = event
if !entering && (node.type.paragraph? || node.type.heading? || node.type.table_cell?)
if !entering && (node.type.paragraph? || node.type.heading? || node.type.table_cell? || node.type.footnote_definition?)
@inline_lexer.parse(node)
end
end
Expand Down
51 changes: 44 additions & 7 deletions src/markd/parsers/inline.cr
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module Markd::Parser
@text = ""
@pos = 0
@refmap = {} of String => Hash(String, String) | String
@footnote_counter = 0
end

def parse(node : Node)
Expand Down Expand Up @@ -151,22 +152,29 @@ module Markd::Parser
private def bang(node : Node)
start_pos = @pos
@pos += 1
if char_at?(@pos) == '['
# It's an image if the next character is a [
# And it's not a footnote OR we are not in GFM mode
#
# * This IS an image: `![...]`
# * This IS an image in non-GFM mode: `![^...]`
# * This IS NOT an image in GFM mode: `![^...]`

if char_at?(@pos) == '[' && (char_at?(@pos + 1) != '^' || !@options.gfm)
@pos += 1
child = text("![")
node.append_child(child)

add_bracket(child, start_pos + 1, true)
add_bracket(child, start_pos + 1, image: true)
else
node.append_child(text("!"))
end

true
end

private def add_bracket(node : Node, index : Int32, image = false)
private def add_bracket(node : Node, index : Int32, *, image = false, footnote = false)
brackets.bracket_after = true if brackets?
@brackets = Bracket.new(node, @brackets, @delimiters, index, image, true)
@brackets = Bracket.new(node, @brackets, @delimiters, index, image: image, active: true, footnote: footnote)
end

private def remove_bracket
Expand All @@ -180,7 +188,11 @@ module Markd::Parser
child = text("[")
node.append_child(child)

add_bracket(child, start_pos, false)
if char_at(@pos) == '^' && @options.gfm
add_bracket(child, start_pos, footnote: true)
else
add_bracket(child, start_pos)
end

true
end
Expand Down Expand Up @@ -210,6 +222,7 @@ module Markd::Parser

# If we got here, open is a potential opener
is_image = opener.image
is_footnote = opener.footnote

# Check to see if we have a link/image
save_pos = @pos
Expand All @@ -228,6 +241,21 @@ module Markd::Parser
end
end

# Is it a footnote?
if is_footnote
# If the 1st char after the closing bracket is a ":" then it's NOT
# a footnote, it's a footnote definition.
if char_at?(@pos) == ':'
@pos = start_pos
node.append_child(text("]"))
remove_bracket
return true
end
title = @text[opener.@index + 2...@pos - 1]
@pos += 1
matched = true
end

ref_label = nil
unless matched
# Next, see if there's a link label
Expand Down Expand Up @@ -257,7 +285,15 @@ module Markd::Parser
end

if matched
child = Node.new(is_image ? Node::Type::Image : Node::Type::Link)
if is_image
child = Node.new(Node::Type::Image)
elsif is_footnote
child = Node.new(Node::Type::Footnote)
child.data["number"] = @footnote_counter += 1
else
child = Node.new(Node::Type::Link)
end

child.data["destination"] = dest.not_nil!
child.data["title"] = title || ""

Expand Down Expand Up @@ -907,8 +943,9 @@ module Markd::Parser
property image : Bool
property active : Bool
property bracket_after : Bool
property footnote : Bool

def initialize(@node, @previous, @previous_delimiter, @index, @image, @active = true)
def initialize(@node, @previous, @previous_delimiter, @index, *, @image = false, @active = true, @footnote = false)
@bracket_after = false
end
end
Expand Down
30 changes: 18 additions & 12 deletions src/markd/renderer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,29 @@ module Markd
false
end

abstract def heading(node : Node, entering : Bool) : Nil
abstract def list(node : Node, entering : Bool) : Nil
abstract def item(node : Node, entering : Bool) : Nil
abstract def block_quote(node : Node, entering : Bool) : Nil
abstract def thematic_break(node : Node, entering : Bool) : Nil
abstract def code_block(node : Node, entering : Bool, formatter : T?) : Nil forall T
abstract def code(node : Node, entering : Bool) : Nil
abstract def emphasis(node : Node, entering : Bool) : Nil
abstract def footnote(node : Node, entering : Bool) : Nil
abstract def footnote_definition(node : Node, entering : Bool) : Nil
abstract def heading(node : Node, entering : Bool) : Nil
abstract def html_block(node : Node, entering : Bool) : Nil
abstract def html_inline(node : Node, entering : Bool) : Nil
abstract def image(node : Node, entering : Bool) : Nil
abstract def item(node : Node, entering : Bool) : Nil
abstract def line_break(node : Node, entering : Bool) : Nil
abstract def link(node : Node, entering : Bool) : Nil
abstract def list(node : Node, entering : Bool) : Nil
abstract def paragraph(node : Node, entering : Bool) : Nil
abstract def emphasis(node : Node, entering : Bool) : Nil
abstract def soft_break(node : Node, entering : Bool) : Nil
abstract def line_break(node : Node, entering : Bool) : Nil
abstract def strong(node : Node, entering : Bool) : Nil
abstract def strikethrough(node : Node, entering : Bool) : Nil
abstract def link(node : Node, entering : Bool) : Nil
abstract def image(node : Node, entering : Bool) : Nil
abstract def text(node : Node, entering : Bool) : Nil
abstract def table(node : Node, entering : Bool) : Nil
abstract def table_row(node : Node, entering : Bool) : Nil
abstract def strong(node : Node, entering : Bool) : Nil
abstract def table_cell(node : Node, entering : Bool) : Nil
abstract def table_row(node : Node, entering : Bool) : Nil
abstract def table(node : Node, entering : Bool) : Nil
abstract def text(node : Node, entering : Bool) : Nil
abstract def thematic_break(node : Node, entering : Bool) : Nil

def render(document : Node, formatter : T?) forall T
Utils.timer("rendering", @options.time) do
Expand Down Expand Up @@ -118,6 +120,10 @@ module Markd
table_row(node, entering)
when Node::Type::TableCell
table_cell(node, entering)
when Node::Type::Footnote
footnote(node, entering)
when Node::Type::FootnoteDefinition
footnote_definition(node, entering)
else
text(node, entering)
end
Expand Down
Loading