diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -257,6 +257,7 @@ 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php', + 'PhutilRemarkupEngineRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupReplyBlockRule.php', 'PhutilRemarkupEngineRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php', 'PhutilRemarkupEngineRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php', 'PhutilRemarkupEngineRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTestInterpreterRule.php', @@ -628,6 +629,7 @@ 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule', + 'PhutilRemarkupEngineRemarkupReplyBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupSimpleTableBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupTableBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter', diff --git a/src/markup/engine/PhutilRemarkupEngine.php b/src/markup/engine/PhutilRemarkupEngine.php --- a/src/markup/engine/PhutilRemarkupEngine.php +++ b/src/markup/engine/PhutilRemarkupEngine.php @@ -1,16 +1,12 @@ metadata = array(); $this->storage = new PhutilRemarkupBlockStorage(); + $blocks = $this->splitTextIntoBlocks($text); + + $output = array(); + foreach ($blocks as $block) { + $output[] = $this->markupBlock($block); + } + $output = $this->flattenOutput($output); + + $map = $this->storage->getMap(); + unset($this->storage); + $metadata = $this->metadata; + + + return array( + 'output' => $output, + 'storage' => $map, + 'metadata' => $metadata, + ); + } + + private function splitTextIntoBlocks($text, $depth = 0) { // Apply basic block and paragraph normalization to the text. NOTE: We don't // strip trailing whitespace because it is semantic in some contexts, // notably inlined diffs that the author intends to show as a code block. - $text = phutil_split_lines($text, true); + $text = phutil_split_lines($text, true); $block_rules = $this->blockRules; - $blocks = array(); - $cursor = 0; - $prev_block = array(); + $blocks = array(); + $cursor = 0; + $prev_block = array(); while (isset($text[$cursor])) { $starting_cursor = $cursor; @@ -139,10 +156,11 @@ } $curr_block = array( - "start" => $cursor, - "num_lines" => $num_lines, - "rule" => $block_rule, - "is_empty" => self::isEmptyBlock($text, $cursor, $num_lines), + 'start' => $cursor, + 'num_lines' => $num_lines, + 'rule' => $block_rule, + 'is_empty' => self::isEmptyBlock($text, $cursor, $num_lines), + 'children' => array(), ); if ($prev_block @@ -164,27 +182,58 @@ } } - $output = array(); - foreach ($blocks as $block) { - $output[] = $block['rule']->markupText( - implode('', array_slice($text, $block['start'], $block['num_lines']))); + foreach ($blocks as $key => $block) { + $lines = array_slice($text, $block['start'], $block['num_lines']); + $blocks[$key]['text'] = implode('', $lines); } - $map = $this->storage->getMap(); - unset($this->storage); - $metadata = $this->metadata; + // Stop splitting child blocks apart if we get too deep. This arrests + // any blocks which have looping child rules, and stops the stack from + // exploding if someone writes a hilarious comment with 5,000 levels of + // quoted text. + + if ($depth < self::MAX_CHILD_DEPTH) { + foreach ($blocks as $key => $block) { + $rule = $block['rule']; + if (!$rule->supportsChildBlocks()) { + continue; + } + + list($parent_text, $child_text) = $rule->extractChildText( + $block['text']); + $blocks[$key]['text'] = $parent_text; + $blocks[$key]['children'] = $this->splitTextIntoBlocks( + $child_text, + $depth + 1); + } + } + + return $blocks; + } + + private function markupBlock(array $block) { + $children = array(); + foreach ($block['children'] as $child) { + $children[] = $this->markupBlock($child); + } + if ($children) { + $children = $this->flattenOutput($children); + } else { + $children = null; + } + + return $block['rule']->markupText($block['text'], $children); + } + + private function flattenOutput(array $output) { if ($this->isTextMode()) { $output = implode("\n\n", $output)."\n"; } else { $output = phutil_implode_html("\n\n", $output); } - return array( - 'output' => $output, - 'storage' => $map, - 'metadata' => $metadata, - ); + return $output; } private static function shouldMergeBlocks($text, $prev_block, $curr_block) { diff --git a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php --- a/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php +++ b/src/markup/engine/__tests__/PhutilRemarkupEngineTestCase.php @@ -90,6 +90,7 @@ $blocks = array(); $blocks[] = new PhutilRemarkupEngineRemarkupQuotesBlockRule(); + $blocks[] = new PhutilRemarkupEngineRemarkupReplyBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupHeaderBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule(); $blocks[] = new PhutilRemarkupEngineRemarkupCodeBlockRule(); diff --git a/src/markup/engine/__tests__/remarkup/reply-basic.txt b/src/markup/engine/__tests__/remarkup/reply-basic.txt new file mode 100644 --- /dev/null +++ b/src/markup/engine/__tests__/remarkup/reply-basic.txt @@ -0,0 +1,12 @@ +>>! In comment #123, alincoln wrote: +> Four score and twenty years ago... +~~~~~~~~~~ +
+
In comment #123, alincoln wrote:
+

Four score and twenty years ago...

+
+~~~~~~~~~~ +In comment #123, alincoln wrote: + +> Four score and twenty years ago... + diff --git a/src/markup/engine/__tests__/remarkup/reply-nested.txt b/src/markup/engine/__tests__/remarkup/reply-nested.txt new file mode 100644 --- /dev/null +++ b/src/markup/engine/__tests__/remarkup/reply-nested.txt @@ -0,0 +1,50 @@ +>>! Previously, fruit: +> +> - Apple +> - Banana +> - Cherry +> +>>>! More previously, vegetables: +>> +>> - Potato +>> - Potato +>> - Potato +> +> The end. + +~~~~~~~~~~ +
+
Previously, fruit:
+
+ +
+
More previously, vegetables:
+
    +
  • Potato
  • +
  • Potato
  • +
  • Potato
  • +
+
+ +

The end.

+
+~~~~~~~~~~ +Previously, fruit: + +> - Apple +> - Banana +> - Cherry +> +> More previously, vegetables: +> +> > - Potato +> > - Potato +> > - Potato +> +> +> The end. + diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php @@ -24,7 +24,7 @@ return 500.0; } - abstract public function markupText($text); + abstract public function markupText($text, $children); /** * This will get an array of unparsed lines and return the number of lines @@ -84,6 +84,14 @@ return $text; } + public function supportsChildBlocks() { + return false; + } + + public function extractChildText($text) { + throw new Exception(pht('Not implemnted!')); + } + protected function renderRemarkupTable(array $out_rows) { assert_instances_of($out_rows, 'array'); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php @@ -46,7 +46,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { if (preg_match('/^```/', $text)) { // If this is a ```-style block, trim off the backticks and any leading // blank line. diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php @@ -14,7 +14,7 @@ return 1; } - public function markupText($text) { + public function markupText($text, $children) { $text = trim($text); $text = $this->applyRules($text); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php @@ -33,7 +33,7 @@ const KEY_HEADER_TOC = 'headers.toc'; - public function markupText($text) { + public function markupText($text, $children) { $text = trim($text); $lines = phutil_split_lines($text); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHorizontalRuleBlockRule.php @@ -29,7 +29,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { return phutil_tag('hr', array()); } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php @@ -10,7 +10,7 @@ return 1; } - public function markupText($text) { + public function markupText($text, $children) { return $this->applyRules($text); } diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInterpreterRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInterpreterRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInterpreterRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInterpreterRule.php @@ -27,7 +27,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { $lines = explode("\n", $text); $first_key = head_key($lines); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php @@ -58,7 +58,7 @@ const CONT_BLOCK_PATTERN = '@^\s*(?:[-*#]+|[0-9]+[.)]|\[.?\])\s+@'; const STRIP_BLOCK_PATTERN = '@^\s*(?:[-*#]+|[0-9]+[.)])\s*@'; - public function markupText($text) { + public function markupText($text, $children) { $items = array(); $lines = explode("\n", $text); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php @@ -24,7 +24,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { $text = preg_replace('/%%%\s*$/', '', substr($text, 3)); if ($this->getEngine()->isTextMode()) { return $text; diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php @@ -26,7 +26,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { $matches = array(); preg_match($this->getRegEx(), $text, $matches); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php @@ -27,7 +27,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { $lines = array(); foreach (explode("\n", $text) as $line) { $lines[] = $this->applyRules(preg_replace('/^>\s*/', '', $line)); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupReplyBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupReplyBlockRule.php new file mode 100644 --- /dev/null +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupReplyBlockRule.php @@ -0,0 +1,93 @@ +>!/', $lines[$pos])) { + do { + ++$pos; + } while (isset($lines[$pos]) && preg_match('/^>/', $lines[$pos])); + } + + return ($pos - $cursor); + } + + public function supportsChildBlocks() { + return true; + } + + public function extractChildText($text) { + $text = phutil_split_lines($text, true); + + $head = array(); + $body = array(); + + $head = substr(reset($text), 3); + + $body = array_slice($text, 1); + + // Remove the carets. + foreach ($body as $key => $line) { + $body[$key] = substr($line, 1); + } + + // Strip leading empty lines. + foreach ($body as $key => $line) { + if (strlen(trim($line))) { + break; + } + unset($body[$key]); + } + + return array(trim($head), implode('', $body)); + } + + public function markupText($text, $children) { + $text = $this->applyRules($text); + + if ($this->getEngine()->isTextMode()) { + $children = phutil_split_lines($children, true); + foreach ($children as $key => $child) { + if (strlen(trim($child))) { + $children[$key] = '> '.$child; + } else { + $children[$key] = '>'.$child; + } + } + $children = implode('', $children); + + return $text."\n\n".$children; + } + + return phutil_tag( + 'blockquote', + array( + 'class' => 'remarkup-reply-block', + ), + array( + "\n", + phutil_tag( + 'div', + array( + 'class' => 'remarkup-reply-head', + ), + $text), + "\n", + phutil_tag( + 'div', + array( + 'class' => 'remarkup-reply-body', + ), + $children), + "\n", + )); + } + +} diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupSimpleTableBlockRule.php @@ -20,7 +20,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { $matches = array(); $rows = array(); diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php @@ -25,7 +25,7 @@ return $num_lines; } - public function markupText($text) { + public function markupText($text, $children) { $matches = array(); if (!preg_match('@^(.*)
$@si', $text, $matches)) {