Something went wrong. Try again.
@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main hq.recaptime.dev/wiki/Phorge
phorge phabricator
Something went wrong. Try again.
9.6 kB · 365 lines
PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366<?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) { // Header/footer eventually useful to be nice with "flavored markdown". // When it starts with ```stuff the header is 'stuff' (->language) // When it ends with stuff``` the footer is 'stuff' (->garbage) $header_line = null; $footer_line = null;
$matches = null; if (preg_match('/^\s*```(.*)/', $text, $matches)) { if (isset($matches[1])) { $header_line = $matches[1]; }
// 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);
// If we have a flavored header, it has sense to look for the footer. if ($header_line !== null && $lines) { $footer_line = $lines[last_key($lines)]; }
// Strip final empty lines 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)); $valid_options = null; if ($custom) { $valid_options = true; foreach ($custom as $key => $value) { if (!array_key_exists($key, $options)) { $valid_options = false; break; } } if ($valid_options) { array_shift($lines); $options = $custom + $options; } }
// Parse flavored markdown strictly to don't eat legitimate Remarkup. // Proceed only if we tried to parse options and we failed // (no options also mean no language). // For example this is not a valid option: ```php // Proceed only if the footer exists and it is not: blabla``` // Accept only 2 lines or more. First line: header; then content. if ( $valid_options === false && $header_line !== null && $footer_line === '' && count($lines) > 1 ) { if (self::isKnownLanguageCode($header_line)) { array_shift($lines); $options['lang'] = $header_line; } }
// 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); }
// The name is usually a sufficient source of information for file ext. if (empty($options['lang']) && isset($options['name'])) { $options['lang'] = $this->guessFilenameExtension($options['name']); }
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))); }
/** * Check if a language code can be used in a generic flavored markdown. * @param string $lang Language code * @return bool */ private static function isKnownLanguageCode($lang) { $languages = self::knownLanguageCodes(); return isset($languages[$lang]); }
/** * Get the available languages for a generic flavored markdown. * @return array Languages as array keys. Ignore the value. */ private static function knownLanguageCodes() { // This is a friendly subset from https://pygments.org/languages/ static $map = array( 'arduino' => 1, 'assembly' => 1, 'awk' => 1, 'bash' => 1, 'bat' => 1, 'c' => 1, 'cmake' => 1, 'cobol' => 1, 'cpp' => 1, 'css' => 1, 'csharp' => 1, 'dart' => 1, 'delphi' => 1, 'fortran' => 1, 'go' => 1, 'groovy' => 1, 'haskell' => 1, 'java' => 1, 'javascript' => 1, 'kotlin' => 1, 'lisp' => 1, 'lua' => 1, 'matlab' => 1, 'make' => 1, 'perl' => 1, 'php' => 1, 'powershell' => 1, 'python' => 1, 'r' => 1, 'ruby' => 1, 'rust' => 1, 'scala' => 1, 'sh' => 1, 'sql' => 1, 'typescript' => 1, 'vba' => 1, ); return $map; }
/** * Get the extension from a filename. * @param string $name "/path/to/something.name" * @return null|string ".name" */ private function guessFilenameExtension($name) { $name = basename($name); $pos = strrpos($name, '.'); if ($pos !== false) { return substr($name, $pos + 1); } return null; }
}