diff --git a/spec/std/markdown/markdown_spec.cr b/spec/std/markdown/markdown_spec.cr
index ea6ada57517e..33fea905eb6e 100644
--- a/spec/std/markdown/markdown_spec.cr
+++ b/spec/std/markdown/markdown_spec.cr
@@ -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
@@ -70,13 +70,14 @@ describe Markdown do
assert_render "Hello\n```\nWorld\n```", "
Hello
\n\nWorld
"
assert_render "> Hello World\n", "Hello World
"
+ assert_render "> This spawns\nmultiple\nlines\n\ntext", "This spawns\nmultiple\nlines
\n\ntext
"
assert_render "* Hello", ""
assert_render "* Hello\n* World", ""
assert_render "* Hello\n* World\n * Crystal", ""
assert_render "* Level1\n * Level2\n * Level2\n* Level1", ""
assert_render "* Level1\n * Level2\n * Level2", ""
- assert_render "* Hello\nWorld", "\n\nWorld
"
+ assert_render "* Hello\nWorld", ""
assert_render "Params:\n* Foo\n* Bar", "Params:
\n\n"
assert_render "+ Hello", ""
@@ -84,11 +85,17 @@ describe Markdown do
assert_render "* Hello\n+ World\n- Crystal", "\n\n\n\n"
+ assert_render "* This spawns\nmultiple\nlines\n\ntext", "- This spawns\nmultiple\nlines
\n\ntext
"
+ assert_render "* Two\nlines\n* This spawns\nmultiple\nlines\n\ntext", "- Two\nlines
- This spawns\nmultiple\nlines
\n\ntext
"
+
assert_render "1. Hello", "- Hello
"
assert_render "2. Hello", "- Hello
"
assert_render "01. Hello\n02. World", "- Hello
- World
"
assert_render "Params:\n 1. Foo\n 2. Bar", "Params:
\n\n- Foo
- Bar
"
+ assert_render "1. This spawns\nmultiple\nlines\n\ntext", "- This spawns\nmultiple\nlines
\n\ntext
"
+ assert_render "1. Two\nlines\n1. This spawns\nmultiple\nlines\n\ntext", "- Two\nlines
- This spawns\nmultiple\nlines
\n\ntext
"
+
assert_render "Hello [world](http://example.com)", %(Hello world
)
assert_render "Hello [world](http://example.com)!", %(Hello world!
)
assert_render "Hello [world **2**](http://example.com)!", %(Hello world 2!
)
@@ -98,6 +105,9 @@ describe Markdown do
assert_render "[![foo](bar)](baz)", %(
)
+ assert_render "This [spawns\nmultiple\nlines](http://example.com)\n\ntext",
+ %(This spawns\nmultiple\nlines
\n\ntext
)
+
assert_render "***", "
"
assert_render "---", "
"
assert_render "___", "
"
diff --git a/src/markdown/parser.cr b/src/markdown/parser.cr
index 63e16b484e67..5df13296a1a6 100644
--- a/src/markdown/parser.cr
+++ b/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)
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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