diff --git a/src/markup/engine/__tests__/remarkup/list-paragraphs.txt b/src/markup/engine/__tests__/remarkup/list-paragraphs.txt new file mode 100644 --- /dev/null +++ b/src/markup/engine/__tests__/remarkup/list-paragraphs.txt @@ -0,0 +1,27 @@ +- This is a list item + with several paragraphs. + + This is the second paragraph + of the first list item. +- This is the second item + in the list. + - This is a sublist. +- This is the third item in the list. + +~~~~~~~~~~ + +~~~~~~~~~~ +- This is a list item with several paragraphs. + + This is the second paragraph of the first list item. +- This is the second item in the list. + - This is a sublist. +- This is the third item in the list. diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php @@ -31,14 +31,29 @@ continue; } - if (strlen(trim($lines[$cursor])) - && strlen($lines[$cursor]) !== strlen(ltrim($lines[$cursor]))) { - $num_lines++; - $cursor++; - continue; + // Allow lists to continue across multiple paragraphs, as long as lines + // are indented or a single empty line separates indented lines. + + $this_empty = !strlen(trim($lines[$cursor])); + $this_indented = preg_match('/^ /', $lines[$cursor]); + + $next_empty = true; + $next_indented = false; + if (isset($lines[$cursor + 1])) { + $next_empty = !strlen(trim($lines[$cursor + 1])); + $next_indented = preg_match('/^ /', $lines[$cursor + 1]); + } + + if ($this_empty || $this_indented) { + if (($this_indented && !$this_empty) || + ($next_indented && !$next_empty)) { + $num_lines++; + $cursor++; + continue; + } } - if (!strlen(trim($lines[$cursor]))) { + if ($this_empty) { $num_lines++; } } @@ -155,6 +170,9 @@ // Process each item to normalize the text, remove line wrapping, and // determine its depth (indentation level) and style (ordered vs unordered). // + // We preserve consecutive linebreaks and interpret them as paragraph + // breaks. + // // Given the above example, the processed array will look like: // // array( @@ -172,7 +190,14 @@ $has_marks = false; foreach ($items as $key => $item) { - $item = preg_replace('/\s*\n\s*/', ' ', implode("\n", $item)); + // Trim space around newlines, to strip trailing whitespace and formatting + // indentation. + $item = preg_replace('/ *(\n+) */', '\1', implode("\n", $item)); + + // Replace single newlines with a space. Preserve multiple newlines as + // paragraph breaks. + $item = preg_replace('/(?applyRules($item['text'])."\n"; + + $parts = preg_split('/\n{2,}/', $item['text']); + foreach ($parts as $key => $part) { + if ($key != 0) { + $out[] = "\n\n ".$indent; + } + $out[] = $this->applyRules($part); + } + $out[] = "\n"; } } else { if ($item['text'] === null) { @@ -469,7 +503,18 @@ $out[] = hsprintf('
  • '); } - $out[] = $this->applyRules($item['text']); + $parts = preg_split('/\n{2,}/', $item['text']); + foreach ($parts as $key => $part) { + if ($key != 0) { + $out[] = array( + "\n", + phutil_tag('br'), + phutil_tag('br'), + "\n", + ); + } + $out[] = $this->applyRules($part); + } } }