Changeset View
Changeset View
Standalone View
Standalone View
src/markup/engine/PhutilRemarkupEngine.php
<?php | <?php | ||||
/** | |||||
* @group markup | |||||
*/ | |||||
final class PhutilRemarkupEngine extends PhutilMarkupEngine { | final class PhutilRemarkupEngine extends PhutilMarkupEngine { | ||||
const MODE_DEFAULT = 0; | const MODE_DEFAULT = 0; | ||||
const MODE_TEXT = 1; | const MODE_TEXT = 1; | ||||
/** | const MAX_CHILD_DEPTH = 8; | ||||
* @var PhutilRemarkupEngineBlockRule[] | |||||
*/ | |||||
private $blockRules = array(); | private $blockRules = array(); | ||||
private $config = array(); | private $config = array(); | ||||
private $mode; | private $mode; | ||||
private $metadata = array(); | private $metadata = array(); | ||||
private $states = array(); | private $states = array(); | ||||
private $postprocessRules = array(); | private $postprocessRules = array(); | ||||
public function setConfig($key, $value) { | public function setConfig($key, $value) { | ||||
▲ Show 20 Lines • Show All 92 Lines • ▼ Show 20 Lines | final class PhutilRemarkupEngine extends PhutilMarkupEngine { | ||||
public function getState($state) { | public function getState($state) { | ||||
return !empty($this->states[$state]); | return !empty($this->states[$state]); | ||||
} | } | ||||
public function preprocessText($text) { | public function preprocessText($text) { | ||||
$this->metadata = array(); | $this->metadata = array(); | ||||
$this->storage = new PhutilRemarkupBlockStorage(); | $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 | // Apply basic block and paragraph normalization to the text. NOTE: We don't | ||||
// strip trailing whitespace because it is semantic in some contexts, | // strip trailing whitespace because it is semantic in some contexts, | ||||
// notably inlined diffs that the author intends to show as a code block. | // 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; | $block_rules = $this->blockRules; | ||||
$blocks = array(); | $blocks = array(); | ||||
$cursor = 0; | $cursor = 0; | ||||
$prev_block = array(); | $prev_block = array(); | ||||
while (isset($text[$cursor])) { | while (isset($text[$cursor])) { | ||||
$starting_cursor = $cursor; | $starting_cursor = $cursor; | ||||
foreach ($block_rules as $block_rule) { | foreach ($block_rules as $block_rule) { | ||||
$num_lines = $block_rule->getMatchingLineCount($text, $cursor); | $num_lines = $block_rule->getMatchingLineCount($text, $cursor); | ||||
if ($num_lines) { | if ($num_lines) { | ||||
if ($blocks) { | if ($blocks) { | ||||
$prev_block = last($blocks); | $prev_block = last($blocks); | ||||
} | } | ||||
$curr_block = array( | $curr_block = array( | ||||
"start" => $cursor, | 'start' => $cursor, | ||||
"num_lines" => $num_lines, | 'num_lines' => $num_lines, | ||||
"rule" => $block_rule, | 'rule' => $block_rule, | ||||
"is_empty" => self::isEmptyBlock($text, $cursor, $num_lines), | 'is_empty' => self::isEmptyBlock($text, $cursor, $num_lines), | ||||
'children' => array(), | |||||
); | ); | ||||
if ($prev_block | if ($prev_block | ||||
&& self::shouldMergeBlocks($text, $prev_block, $curr_block)) { | && self::shouldMergeBlocks($text, $prev_block, $curr_block)) { | ||||
$blocks[last_key($blocks)]["num_lines"] += $curr_block["num_lines"]; | $blocks[last_key($blocks)]["num_lines"] += $curr_block["num_lines"]; | ||||
$blocks[last_key($blocks)]["is_empty"] = | $blocks[last_key($blocks)]["is_empty"] = | ||||
$blocks[last_key($blocks)]["is_empty"] && $curr_block["is_empty"]; | $blocks[last_key($blocks)]["is_empty"] && $curr_block["is_empty"]; | ||||
} else { | } else { | ||||
$blocks[] = $curr_block; | $blocks[] = $curr_block; | ||||
} | } | ||||
$cursor += $num_lines; | $cursor += $num_lines; | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
if ($starting_cursor === $cursor) { | if ($starting_cursor === $cursor) { | ||||
throw new Exception("Block in text did not match any block rule."); | throw new Exception("Block in text did not match any block rule."); | ||||
} | } | ||||
} | } | ||||
$output = array(); | foreach ($blocks as $key => $block) { | ||||
foreach ($blocks as $block) { | $lines = array_slice($text, $block['start'], $block['num_lines']); | ||||
$output[] = $block['rule']->markupText( | $blocks[$key]['text'] = implode('', $lines); | ||||
implode('', array_slice($text, $block['start'], $block['num_lines']))); | |||||
} | } | ||||
$map = $this->storage->getMap(); | // Stop splitting child blocks apart if we get too deep. This arrests | ||||
unset($this->storage); | // any blocks which have looping child rules, and stops the stack from | ||||
$metadata = $this->metadata; | // 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()) { | if ($this->isTextMode()) { | ||||
$output = implode("\n\n", $output)."\n"; | $output = implode("\n\n", $output)."\n"; | ||||
} else { | } else { | ||||
$output = phutil_implode_html("\n\n", $output); | $output = phutil_implode_html("\n\n", $output); | ||||
} | } | ||||
return array( | return $output; | ||||
'output' => $output, | |||||
'storage' => $map, | |||||
'metadata' => $metadata, | |||||
); | |||||
} | } | ||||
private static function shouldMergeBlocks($text, $prev_block, $curr_block) { | private static function shouldMergeBlocks($text, $prev_block, $curr_block) { | ||||
$block_rules = ipull(array($prev_block, $curr_block), "rule"); | $block_rules = ipull(array($prev_block, $curr_block), "rule"); | ||||
$default_rule = "PhutilRemarkupEngineRemarkupDefaultBlockRule"; | $default_rule = "PhutilRemarkupEngineRemarkupDefaultBlockRule"; | ||||
try { | try { | ||||
assert_instances_of($block_rules, $default_rule); | assert_instances_of($block_rules, $default_rule); | ||||
▲ Show 20 Lines • Show All 54 Lines • Show Last 20 Lines |