From fbdc73673e808993f42097a8f296d5fb340a722c Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Mon, 5 Dec 2016 09:10:49 -0300 Subject: [PATCH] Markdown: correctly group paragraphs regarding lists, quotes and links --- spec/std/markdown/markdown_spec.cr | 14 ++- src/markdown/parser.cr | 136 ++++++++++++++++++----------- 2 files changed, 98 insertions(+), 52 deletions(-) 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\n
World
" assert_render "> Hello World\n", "
Hello World
" + assert_render "> This spawns\nmultiple\nlines\n\ntext", "
This spawns\nmultiple\nlines
\n\n

text

" 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\n

World

" + 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", "\n\n

text

" + assert_render "* Two\nlines\n* This spawns\nmultiple\nlines\n\ntext", "\n\n

text

" + assert_render "1. Hello", "
  1. Hello
" assert_render "2. Hello", "
  1. Hello
" assert_render "01. Hello\n02. World", "
  1. Hello
  2. World
" assert_render "Params:\n 1. Foo\n 2. Bar", "

Params:

\n\n
  1. Foo
  2. Bar
" + assert_render "1. This spawns\nmultiple\nlines\n\ntext", "
  1. This spawns\nmultiple\nlines
\n\n

text

" + assert_render "1. Two\nlines\n1. This spawns\nmultiple\nlines\n\ntext", "
  1. Two\nlines
  2. This spawns\nmultiple\nlines
\n\n

text

" + 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)", %(

foo

) + assert_render "This [spawns\nmultiple\nlines](http://example.com)\n\ntext", + %(

This spawns\nmultiple\nlines

\n\n

text

) + 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