Skip to content

Commit

Permalink
Markdown: correctly group paragraphs regarding lists, quotes and links
Browse files Browse the repository at this point in the history
  • Loading branch information
asterite committed Dec 5, 2016
1 parent 35c8d92 commit fbdc736
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 52 deletions.
14 changes: 12 additions & 2 deletions spec/std/markdown/markdown_spec.cr
Expand Up @@ -3,7 +3,7 @@ require "markdown"

private def assert_render(input, output, file = __FILE__, line = __LINE__)
it "renders #{input.inspect}", file, line do
Markdown.to_html(input).should eq(output)
Markdown.to_html(input).should eq(output), file, line
end
end

Expand Down Expand Up @@ -70,25 +70,32 @@ describe Markdown do
assert_render "Hello\n```\nWorld\n```", "<p>Hello</p>\n\n<pre><code>World</code></pre>"

assert_render "> Hello World\n", "<blockquote>Hello World</blockquote>"
assert_render "> This spawns\nmultiple\nlines\n\ntext", "<blockquote>This spawns\nmultiple\nlines</blockquote>\n\n<p>text</p>"

assert_render "* Hello", "<ul><li>Hello</li></ul>"
assert_render "* Hello\n* World", "<ul><li>Hello</li><li>World</li></ul>"
assert_render "* Hello\n* World\n * Crystal", "<ul><li>Hello</li><li>World</li><ul><li>Crystal</li></ul></ul>"
assert_render "* Level1\n * Level2\n * Level2\n* Level1", "<ul><li>Level1</li><ul><li>Level2</li><li>Level2</li></ul><li>Level1</li></ul>"
assert_render "* Level1\n * Level2\n * Level2", "<ul><li>Level1</li><ul><li>Level2</li><li>Level2</li></ul></ul>"
assert_render "* Hello\nWorld", "<ul><li>Hello</li></ul>\n\n<p>World</p>"
assert_render "* Hello\nWorld", "<ul><li>Hello\nWorld</li></ul>"
assert_render "Params:\n* Foo\n* Bar", "<p>Params:</p>\n\n<ul><li>Foo</li><li>Bar</li></ul>"

assert_render "+ Hello", "<ul><li>Hello</li></ul>"
assert_render "- Hello", "<ul><li>Hello</li></ul>"

assert_render "* Hello\n+ World\n- Crystal", "<ul><li>Hello</li></ul>\n\n<ul><li>World</li></ul>\n\n<ul><li>Crystal</li></ul>"

assert_render "* This spawns\nmultiple\nlines\n\ntext", "<ul><li>This spawns\nmultiple\nlines</li></ul>\n\n<p>text</p>"
assert_render "* Two\nlines\n* This spawns\nmultiple\nlines\n\ntext", "<ul><li>Two\nlines</li><li>This spawns\nmultiple\nlines</li></ul>\n\n<p>text</p>"

assert_render "1. Hello", "<ol><li>Hello</li></ol>"
assert_render "2. Hello", "<ol><li>Hello</li></ol>"
assert_render "01. Hello\n02. World", "<ol><li>Hello</li><li>World</li></ol>"
assert_render "Params:\n 1. Foo\n 2. Bar", "<p>Params:</p>\n\n<ol><li>Foo</li><li>Bar</li></ol>"

assert_render "1. This spawns\nmultiple\nlines\n\ntext", "<ol><li>This spawns\nmultiple\nlines</li></ol>\n\n<p>text</p>"
assert_render "1. Two\nlines\n1. This spawns\nmultiple\nlines\n\ntext", "<ol><li>Two\nlines</li><li>This spawns\nmultiple\nlines</li></ol>\n\n<p>text</p>"

assert_render "Hello [world](http://example.com)", %(<p>Hello <a href="http://example.com">world</a></p>)
assert_render "Hello [world](http://example.com)!", %(<p>Hello <a href="http://example.com">world</a>!</p>)
assert_render "Hello [world **2**](http://example.com)!", %(<p>Hello <a href="http://example.com">world <strong>2</strong></a>!</p>)
Expand All @@ -98,6 +105,9 @@ describe Markdown do

assert_render "[![foo](bar)](baz)", %(<p><a href="baz"><img src="bar" alt="foo"/></a></p>)

assert_render "This [spawns\nmultiple\nlines](http://example.com)\n\ntext",
%(<p>This <a href="http://example.com">spawns\nmultiple\nlines</a></p>\n\n<p>text</p>)

assert_render "***", "<hr/>"
assert_render "---", "<hr/>"
assert_render "___", "<hr/>"
Expand Down
136 changes: 86 additions & 50 deletions src/markdown/parser.cr
@@ -1,4 +1,7 @@
class Markdown::Parser
record PrefixHeader, count : Int32
record UnorderedList, char : Char

@lines : Array(String)

def initialize(text : String, @renderer : Renderer)
Expand All @@ -15,57 +18,82 @@ class Markdown::Parser
def process_paragraph
line = @lines[@line]

if empty? line
case item = classify(line)
when :empty
@line += 1
return
when :header1
render_header 1, line, 2
when :header2
render_header 2, line, 2
when PrefixHeader
render_prefix_header(item.count, line)
when :code
render_code
when :horizontal_rule
render_horizontal_rule
when UnorderedList
render_unordered_list(item.char)
when :fenced_code
render_fenced_code
when :ordered_list
render_ordered_list
when :quote
render_quote
else
render_paragraph
end
end

def classify(line)
if empty? line
return :empty
end

if next_line_is_all?('=')
return render_header 1, line, 2
return :header1
end

if next_line_is_all?('-')
return render_header 2, line, 2
return :header2
end

pounds = count_pounds line
if pounds
return render_prefix_header pounds, line
if pounds = count_pounds line
return PrefixHeader.new(pounds)
end

if line.starts_with? " "
return render_code
return :code
end

if horizontal_rule? line
return render_horizontal_rule
return :horizontal_rule
end

if starts_with_bullet_list_marker?(line, '*')
return render_unordered_list('*')
return UnorderedList.new('*')
end

if starts_with_bullet_list_marker?(line, '+')
return render_unordered_list('+')
return UnorderedList.new('+')
end

if starts_with_bullet_list_marker?(line, '-')
return render_unordered_list('-')
return UnorderedList.new('-')
end

if starts_with_backticks? line
return render_fenced_code
return :fenced_code
end

if starts_with_digits_dot? line
return render_ordered_list
return :ordered_list
end

if line.starts_with? ">"
return render_quote
return :quote
end

render_paragraph
nil
end

def render_prefix_header(level, line)
Expand All @@ -91,27 +119,9 @@ class Markdown::Parser
def render_paragraph
@renderer.begin_paragraph

while true
process_line @lines[@line]
@line += 1

if @line == @lines.size
break
end

line = @lines[@line]

if empty? line
@line += 1
break
end

if (starts_with_bullet_list_marker?(line) || starts_with_backticks?(line) || starts_with_digits_dot?(line))
break
end

newline
end
join_next_lines continue_on: nil
process_line @lines[@line]
@line += 1

@renderer.end_paragraph

Expand Down Expand Up @@ -185,20 +195,11 @@ class Markdown::Parser
def render_quote
@renderer.begin_quote

while true
line = @lines[@line]

break unless line.starts_with? ">"

@renderer.text line.byte_slice(Math.min(line.bytesize, 2))
@line += 1

if @line == @lines.size
break
end
join_next_lines continue_on: :quote
line = @lines[@line]

newline
end
@renderer.text line.byte_slice(Math.min(line.bytesize, 2))
@line += 1

@renderer.end_quote

Expand All @@ -209,6 +210,7 @@ class Markdown::Parser
@renderer.begin_unordered_list

while true
join_next_lines continue_on: nil, stop_on: UnorderedList.new(prefix)
line = @lines[@line]

if empty? line
Expand Down Expand Up @@ -251,6 +253,7 @@ class Markdown::Parser
@renderer.begin_ordered_list

while true
join_next_lines continue_on: nil, stop_on: :ordered_list
line = @lines[@line]

if empty? line
Expand Down Expand Up @@ -604,4 +607,37 @@ class Markdown::Parser
def newline
@renderer.text "\n"
end

# Join this line with next lines if they form a paragraph,
# until next lines don't start another entity like a list,
# header, etc.
def join_next_lines(continue_on = :none, stop_on = :none)
start = @line
line = @line
line += 1
while line < @lines.size
item = classify(@lines[line])

case item
when continue_on
# continue
when stop_on
line -= 1
break
when nil
# paragraph: continue
else
line -= 1
break
end

line += 1
end
line -= 1 if line == @lines.size

if line > start
@lines[line] = (start..line).join("\n") { |i| @lines[i] }
@line = line
end
end
end

0 comments on commit fbdc736

Please sign in to comment.