diff --git a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php index d9c24cb..a09e69b 100644 --- a/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php @@ -1,163 +1,164 @@ getBlockPattern(), $block)) { return false; } if (preg_match('@^[a-z]+://\S+$@', trim($block))) { return false; } return true; } public function shouldContinueWithBlock($block, $last_block) { // If the first code block begins with ```, we keep matching blocks until // we hit a terminating ```, regardless of their content. if (preg_match('/^```/', $last_block)) { if (preg_match('/```$/', $last_block)) { return false; } return true; } // If we just matched a code block based on indentation, always match the // next block if it is indented, too. This basically means that we'll treat // lists after code blocks as more code, but usually the "-" is from a diff // or from objective C or something; it is rare to intentionally follow a // code block with a list. if (preg_match('/^\s{2,}/', $block)) { return true; } return false; } public function shouldMergeBlocks() { return true; } public function markupText($text) { if (preg_match('/^```/', $text)) { // If this is a ```-style block, trim off the backticks. $text = preg_replace('/```\s*$/', '', substr($text, 3)); } $lines = explode("\n", $text); $options = array( 'counterexample' => false, 'lang' => null, 'name' => null, 'lines' => null, ); - $custom = PhutilSimpleOptions::parse(head($lines)); + $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; } } if ($options['counterexample']) { $aux_class = ' remarkup-counterexample'; } else { $aux_class = null; } // 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; } } } if ($min_indent) { $indent_string = str_repeat(' ', $min_indent); $text = preg_replace( '/^'.$indent_string.'/m', '', implode("\n", $lines)); } else { $text = implode("\n", $lines); } 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'), 'php'); } $options['lang'] = $lang; } $name_header = null; if ($options['name']) { $name_header = phutil_render_tag( 'div', array( 'class' => 'remarkup-code-header', ), phutil_escape_html($options['name'])); } $aux_style = null; if ($options['lines']) { // Put a minimum size on this because the scrollbar is otherwise // unusable. $height = max(6, (int)$options['lines']); $aux_style = 'max-height: '.(2 * $height).'em;'; } $engine = $this->getEngine()->getConfig('syntax-highlighter.engine'); if (!$engine) { $engine = 'PhutilDefaultSyntaxHighlighterEngine'; } $engine = newv($engine, array()); $engine->setConfig( 'pygments.enabled', $this->getEngine()->getConfig('pygments.enabled')); $code_body = phutil_render_tag( 'pre', array( 'class' => 'remarkup-code'.$aux_class, 'style' => $aux_style, ), $engine->highlightSource($options['lang'], $text)); return phutil_render_tag( 'div', array( 'class' => 'remarkup-code-block', 'data-code-lang' => $options['lang'], ), $name_header.$code_body); } } diff --git a/src/parser/PhutilSimpleOptions.php b/src/parser/PhutilSimpleOptions.php index 041cd4a..d0b6122 100644 --- a/src/parser/PhutilSimpleOptions.php +++ b/src/parser/PhutilSimpleOptions.php @@ -1,111 +1,146 @@ '4', * 'eyes' => '2', * ); * * @param string Input option list. * @return dict Parsed dictionary. * @task parse */ - public static function parse($input) { + public function parse($input) { $result = array(); $vars = explode(',', $input); foreach ($vars as $var) { if (strpos($var, '=') !== false) { list($key, $value) = explode('=', $var, 2); $value = trim($value); } else { list($key, $value) = array($var, true); } - $key = trim($key); - $key = strtolower($key); - if (!self::isValidKey($key)) { + $key = $this->normalizeKey($key); + if (!$this->isValidKey($key)) { // If there are bad keys, just bail, so we don't get silly results for // parsing inputs like "SELECT id, name, size FROM table". return array(); } if (!strlen($value)) { unset($result[$key]); continue; } $result[$key] = $value; } return $result; } /* -( Unparsing Simple Options )------------------------------------------- */ /** * Convert a dictionary into a simple option list. For example: * * array( * 'legs' => '4', * 'eyes' => '2', * ); * * ...becomes: * * legs=4, eyes=2 * * @param dict Input dictionary. * @return string Unparsed option list. */ - public static function unparse(array $options) { + public function unparse(array $options) { $result = array(); foreach ($options as $name => $value) { - if (!self::isValidKey($name)) { + if (!$this->isValidKey($name)) { throw new Exception( - "SimpleOptions: keys must contain only lowercase letters."); + "SimpleOptions: keys '{$name}' is not valid."); } if (!strlen($value)) { continue; } if ($value === true) { $result[] = $name; } else { $result[] = $name.'='.$value; } } return implode(', ', $result); } +/* -( Parser Configuration )----------------------------------------------- */ + + + /** + * Configure case sensitivity of the parser. By default, the parser is + * case insensitive, so "legs=4" has the same meaning as "LEGS=4". If you + * set it to be case sensitive, the keys have different meanings. + * + * @param bool True to make the parser case sensitive, false (default) to + * make it case-insensitive. + * @return this + * @task config + */ + public function setCaseSensitive($case_sensitive) { + $this->caseSensitive = $case_sensitive; + return $this; + } + + /* -( Internals )---------------------------------------------------------- */ - private static function isValidKey($key) { - return (bool)preg_match('/^[a-z]+$/', $key); + private function isValidKey($key) { + if (!$this->caseSensitive) { + $regexp = '/^[a-z]+$/'; + } else { + $regexp = '/^[a-z]+$/i'; + } + + return (bool)preg_match($regexp, $key); + } + + private function normalizeKey($key) { + $key = trim($key); + if (!$this->caseSensitive) { + $key = strtolower($key); + } + return $key; } } diff --git a/src/parser/__tests__/PhutilSimpleOptionsTestCase.php b/src/parser/__tests__/PhutilSimpleOptionsTestCase.php index ee36bad..f3ab7dd 100644 --- a/src/parser/__tests__/PhutilSimpleOptionsTestCase.php +++ b/src/parser/__tests__/PhutilSimpleOptionsTestCase.php @@ -1,86 +1,108 @@ array(), // Basic parsing. 'legs=4' => array('legs' => '4'), 'legs=4,eyes=2' => array('legs' => '4', 'eyes' => '2'), // Repeated keys mean last specification wins. 'legs=4,legs=3' => array('legs' => '3'), // Keys with no value should map to true. 'flag' => array('flag' => true), 'legs=4,flag' => array('legs' => '4', 'flag' => true), // Spaces should be ignored. ' flag ' => array('flag' => true), ' legs = 4 , eyes = 2' => array('legs' => '4', 'eyes' => '2'), // Case should be ignored. 'LEGS=4' => array('legs' => '4'), 'legs=4, LEGS=4' => array('legs' => '4'), // Empty values should be absent. 'legs=' => array(), 'legs=4,legs=,eyes=2' => array('eyes' => '2'), // Strings like this should not parse as simpleoptions. 'SELECT id, name, size FROM table' => array(), ); foreach ($map as $string => $expect) { + $parser = new PhutilSimpleOptions(); $this->assertEqual( $expect, - PhutilSimpleOptions::parse($string), + $parser->parse($string), "Correct parse of '{$string}'"); } } + public function testSimpleOptionsCaseParse() { + $map = array( + 'legs=4, LEGS=8, LeGs' => array( + 'legs' => '4', + 'LEGS' => '8', + 'LeGs' => true, + ), + ); + + foreach ($map as $string => $expect) { + $parser = new PhutilSimpleOptions(); + $parser->setCaseSensitive(true); + $this->assertEqual( + $expect, + $parser->parse($string), + "Correct case-sensitive parse of '{$string}'"); + } + } + public function testSimpleOptionsUnparse() { $map = array( '' => array(), 'legs=4' => array('legs' => '4'), 'legs=4, eyes=2' => array('legs' => '4', 'eyes' => '2'), 'eyes=2, legs=4' => array('eyes' => '2', 'legs' => '4'), 'legs=4, head' => array('legs' => '4', 'head' => true), 'eyes=2' => array('legs' => '', 'eyes' => '2'), ); foreach ($map as $expect => $dict) { + $parser = new PhutilSimpleOptions(); $this->assertEqual( $expect, - PhutilSimpleOptions::unparse($dict), + $parser->unparse($dict), "Correct unparse of ".print_r($dict, true)); } $bogus = array( array('LEGS' => true), array('LEGS' => 4), array('!' => '!'), array('' => '2'), ); foreach ($bogus as $bad_input) { $caught = null; try { - PhutilSimpleOptions::unparse($bad_input); + $parser = new PhutilSimpleOptions(); + $parser->unparse($bad_input); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, $caught instanceof Exception, "Correct throw on unparse of bad input."); } } }