Changeset View
Changeset View
Standalone View
Standalone View
src/infrastructure/markup/blockrule/PhutilRemarkupCodeBlockRule.php
- This file was added.
| <?php | |||||
| final class PhutilRemarkupCodeBlockRule extends PhutilRemarkupBlockRule { | |||||
| public function getMatchingLineCount(array $lines, $cursor) { | |||||
| $num_lines = 0; | |||||
| $match_ticks = null; | |||||
| if (preg_match('/^(\s{2,}).+/', $lines[$cursor])) { | |||||
| $match_ticks = false; | |||||
| } else if (preg_match('/^\s*(```)/', $lines[$cursor])) { | |||||
| $match_ticks = true; | |||||
| } else { | |||||
| return $num_lines; | |||||
| } | |||||
| $num_lines++; | |||||
| if ($match_ticks && | |||||
| preg_match('/^\s*(```)(.*)(```)\s*$/', $lines[$cursor])) { | |||||
| return $num_lines; | |||||
| } | |||||
| $cursor++; | |||||
| while (isset($lines[$cursor])) { | |||||
| if ($match_ticks) { | |||||
| if (preg_match('/```\s*$/', $lines[$cursor])) { | |||||
| $num_lines++; | |||||
| break; | |||||
| } | |||||
| $num_lines++; | |||||
| } else { | |||||
| if (strlen(trim($lines[$cursor]))) { | |||||
| if (!preg_match('/^\s{2,}/', $lines[$cursor])) { | |||||
| break; | |||||
| } | |||||
| } | |||||
| $num_lines++; | |||||
| } | |||||
| $cursor++; | |||||
| } | |||||
| return $num_lines; | |||||
| } | |||||
| public function markupText($text, $children) { | |||||
| if (preg_match('/^\s*```/', $text)) { | |||||
| // If this is a ```-style block, trim off the backticks and any leading | |||||
| // blank line. | |||||
| $text = preg_replace('/^\s*```(\s*\n)?/', '', $text); | |||||
| $text = preg_replace('/```\s*$/', '', $text); | |||||
| } | |||||
| $lines = explode("\n", $text); | |||||
| while ($lines && !strlen(last($lines))) { | |||||
| unset($lines[last_key($lines)]); | |||||
| } | |||||
| $options = array( | |||||
| 'counterexample' => false, | |||||
| 'lang' => null, | |||||
| 'name' => null, | |||||
| 'lines' => null, | |||||
| ); | |||||
| $parser = new PhutilSimpleOptions(); | |||||
| $custom = $parser->parse(head($lines)); | |||||
| if ($custom) { | |||||
| $valid = true; | |||||
| foreach ($custom as $key => $value) { | |||||
| if (!array_key_exists($key, $options)) { | |||||
| $valid = false; | |||||
| break; | |||||
| } | |||||
| } | |||||
| if ($valid) { | |||||
| array_shift($lines); | |||||
| $options = $custom + $options; | |||||
| } | |||||
| } | |||||
| // Normalize the text back to a 0-level indent. | |||||
| $min_indent = 80; | |||||
| foreach ($lines as $line) { | |||||
| for ($ii = 0; $ii < strlen($line); $ii++) { | |||||
| if ($line[$ii] != ' ') { | |||||
| $min_indent = min($ii, $min_indent); | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| $text = implode("\n", $lines); | |||||
| if ($min_indent) { | |||||
| $indent_string = str_repeat(' ', $min_indent); | |||||
| $text = preg_replace('/^'.$indent_string.'/m', '', $text); | |||||
| } | |||||
| if ($this->getEngine()->isTextMode()) { | |||||
| $out = array(); | |||||
| $header = array(); | |||||
| if ($options['counterexample']) { | |||||
| $header[] = 'counterexample'; | |||||
| } | |||||
| if ($options['name'] != '') { | |||||
| $header[] = 'name='.$options['name']; | |||||
| } | |||||
| if ($header) { | |||||
| $out[] = implode(', ', $header); | |||||
| } | |||||
| $text = preg_replace('/^/m', ' ', $text); | |||||
| $out[] = $text; | |||||
| return implode("\n", $out); | |||||
| } | |||||
| if (empty($options['lang'])) { | |||||
| // If the user hasn't specified "lang=..." explicitly, try to guess the | |||||
| // language. If we fail, fall back to configured defaults. | |||||
| $lang = PhutilLanguageGuesser::guessLanguage($text); | |||||
| if (!$lang) { | |||||
| $lang = nonempty( | |||||
| $this->getEngine()->getConfig('phutil.codeblock.language-default'), | |||||
| 'text'); | |||||
| } | |||||
| $options['lang'] = $lang; | |||||
| } | |||||
| $code_body = $this->highlightSource($text, $options); | |||||
| $name_header = null; | |||||
| $block_style = null; | |||||
| if ($this->getEngine()->isHTMLMailMode()) { | |||||
| $map = $this->getEngine()->getConfig('phutil.codeblock.style-map'); | |||||
| if ($map) { | |||||
| $raw_body = id(new PhutilPygmentizeParser()) | |||||
| ->setMap($map) | |||||
| ->parse((string)$code_body); | |||||
| $code_body = phutil_safe_html($raw_body); | |||||
| } | |||||
| $style_rules = array( | |||||
| 'padding: 6px 12px;', | |||||
| 'font-size: 13px;', | |||||
| 'font-weight: bold;', | |||||
| 'display: inline-block;', | |||||
| 'border-top-left-radius: 3px;', | |||||
| 'border-top-right-radius: 3px;', | |||||
| 'color: rgba(0,0,0,.75);', | |||||
| ); | |||||
| if ($options['counterexample']) { | |||||
| $style_rules[] = 'background: #f7e6e6'; | |||||
| } else { | |||||
| $style_rules[] = 'background: rgba(71, 87, 120, 0.08);'; | |||||
| } | |||||
| $header_attributes = array( | |||||
| 'style' => implode(' ', $style_rules), | |||||
| ); | |||||
| $block_style = 'margin: 12px 0;'; | |||||
| } else { | |||||
| $header_attributes = array( | |||||
| 'class' => 'remarkup-code-header', | |||||
| ); | |||||
| } | |||||
| if ($options['name']) { | |||||
| $name_header = phutil_tag( | |||||
| 'div', | |||||
| $header_attributes, | |||||
| $options['name']); | |||||
| } | |||||
| $class = 'remarkup-code-block'; | |||||
| if ($options['counterexample']) { | |||||
| $class = 'remarkup-code-block code-block-counterexample'; | |||||
| } | |||||
| $attributes = array( | |||||
| 'class' => $class, | |||||
| 'style' => $block_style, | |||||
| 'data-code-lang' => $options['lang'], | |||||
| 'data-sigil' => 'remarkup-code-block', | |||||
| ); | |||||
| return phutil_tag( | |||||
| 'div', | |||||
| $attributes, | |||||
| array($name_header, $code_body)); | |||||
| } | |||||
| private function highlightSource($text, array $options) { | |||||
| if ($options['counterexample']) { | |||||
| $aux_class = ' remarkup-counterexample'; | |||||
| } else { | |||||
| $aux_class = null; | |||||
| } | |||||
| $aux_style = null; | |||||
| if ($this->getEngine()->isHTMLMailMode()) { | |||||
| $aux_style = array( | |||||
| 'font: 11px/15px "Menlo", "Consolas", "Monaco", monospace;', | |||||
| 'padding: 12px;', | |||||
| 'margin: 0;', | |||||
| ); | |||||
| if ($options['counterexample']) { | |||||
| $aux_style[] = 'background: #f7e6e6;'; | |||||
| } else { | |||||
| $aux_style[] = 'background: rgba(71, 87, 120, 0.08);'; | |||||
| } | |||||
| $aux_style = implode(' ', $aux_style); | |||||
| } | |||||
| if ($options['lines']) { | |||||
| // Put a minimum size on this because the scrollbar is otherwise | |||||
| // unusable. | |||||
| $height = max(6, (int)$options['lines']); | |||||
| $aux_style = $aux_style | |||||
| .' ' | |||||
| .'max-height: ' | |||||
| .(2 * $height) | |||||
| .'em; overflow: auto;'; | |||||
| } | |||||
| $engine = $this->getEngine()->getConfig('syntax-highlighter.engine'); | |||||
| if (!$engine) { | |||||
| $engine = 'PhutilDefaultSyntaxHighlighterEngine'; | |||||
| } | |||||
| $engine = newv($engine, array()); | |||||
| $engine->setConfig( | |||||
| 'pygments.enabled', | |||||
| $this->getEngine()->getConfig('pygments.enabled')); | |||||
| return phutil_tag( | |||||
| 'pre', | |||||
| array( | |||||
| 'class' => 'remarkup-code'.$aux_class, | |||||
| 'style' => $aux_style, | |||||
| ), | |||||
| PhutilSafeHTML::applyFunction( | |||||
| 'rtrim', | |||||
| $engine->highlightSource($options['lang'], $text))); | |||||
| } | |||||
| } | |||||