diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index cd6b2231..30cc6219 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,121 +1,125 @@ array( 'CommandException' => 'future/exec', 'ConduitClient' => 'conduit/client', 'ConduitClientException' => 'conduit/client', 'ConduitFuture' => 'conduit/client', 'ExecFuture' => 'future/exec', 'FileFinder' => 'filesystem/filefinder', 'FileList' => 'filesystem/filelist', 'Filesystem' => 'filesystem', 'FilesystemException' => 'filesystem', 'Future' => 'future', 'FutureIterator' => 'future', 'HTTPFuture' => 'future/http', 'PhutilConsoleFormatter' => 'console', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/default', + 'PhutilDocblockParser' => 'parser/docblock', + 'PhutilDocblockParserTestCase' => 'parser/docblock/__tests__', 'PhutilInteractiveEditor' => 'console/editor', 'PhutilMarkupEngine' => 'markup/engine', 'PhutilMissingSymbolException' => 'symbols/exception/missing', 'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/blockstorage', 'PhutilRemarkupEngine' => 'markup/engine/remarkup', 'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/base', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/remarkupcode', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/remarkupdefault', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/remarkupheader', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/remarkupinline', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/remarkuplist', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/base', 'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/bold', 'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/escapehtml', 'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/escaperemarkup', 'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/hyperlink', 'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/italics', 'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/linebreaks', 'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/monospace', 'PhutilSymbolLoader' => 'symbols', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/base', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/base', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/xhpast', 'TempFile' => 'filesystem/tempfile', 'XHPASTNode' => 'parser/xhpast/api/node', 'XHPASTNodeList' => 'parser/xhpast/api/list', 'XHPASTSyntaxErrorException' => 'parser/xhpast/api/exception', 'XHPASTToken' => 'parser/xhpast/api/token', 'XHPASTTree' => 'parser/xhpast/api/tree', 'XHPASTTreeTestCase' => 'parser/xhpast/api/tree/__tests__', ), 'function' => array( 'Futures' => 'future', 'array_select_keys' => 'utils', 'coalesce' => 'utils', 'csprintf' => 'xsprintf/csprintf', 'exec_manual' => 'future/exec', 'execx' => 'future/exec', 'id' => 'utils', 'idx' => 'utils', + 'igroup' => 'utils', 'ipull' => 'utils', 'jsprintf' => 'xsprintf/jsprintf', 'mgroup' => 'utils', 'mpull' => 'utils', 'msort' => 'utils', 'newv' => 'utils', 'nonempty' => 'utils', 'phutil_autoload_class' => 'autoload', 'phutil_console_confirm' => 'console', 'phutil_console_format' => 'console', 'phutil_console_prompt' => 'console', 'phutil_console_wrap' => 'console', 'phutil_escape_html' => 'markup', 'phutil_escape_uri' => 'markup', 'phutil_get_library_name_for_root' => 'moduleutils', 'phutil_get_library_root' => 'moduleutils', 'phutil_get_library_root_for_path' => 'moduleutils', 'phutil_render_tag' => 'markup', 'vcsprintf' => 'xsprintf/csprintf', 'vjsprintf' => 'xsprintf/jsprintf', 'xhp_parser_node_constants' => 'parser/xhpast/constants', 'xhpast_get_binary_path' => 'parser/xhpast/bin', 'xhpast_get_build_instructions' => 'parser/xhpast/bin', 'xhpast_get_parser_future' => 'parser/xhpast/bin', 'xhpast_is_available' => 'parser/xhpast/bin', 'xhpast_parser_token_constants' => 'parser/xhpast/constants', 'xsprintf' => 'xsprintf', 'xsprintf_callback_example' => 'xsprintf', 'xsprintf_command' => 'xsprintf/csprintf', 'xsprintf_javascript' => 'xsprintf/jsprintf', ), 'requires_class' => array( 'ConduitFuture' => 'HTTPFuture', 'ExecFuture' => 'Future', 'HTTPFuture' => 'Future', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', + 'PhutilDocblockParserTestCase' => 'ArcanistPhutilTestCase', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule', 'XHPASTTreeTestCase' => 'ArcanistPhutilTestCase', ), 'requires_interface' => array( ), )); diff --git a/src/parser/docblock/PhutilDocblockParser.php b/src/parser/docblock/PhutilDocblockParser.php new file mode 100644 index 00000000..af588bb3 --- /dev/null +++ b/src/parser/docblock/PhutilDocblockParser.php @@ -0,0 +1,97 @@ + $line) { + if (preg_match('/^\s*@\w/i', $line)) { + $last = $k; + continue; + } else if (preg_match('/^\s*$/', $line)) { + $last = false; + } else if ($last) { + $lines[$last] = rtrim($lines[$last]).' '.trim($line); + unset($lines[$k]); + } + } + $docblock = implode("\n", $lines); + + $special = array(); + + // Parse @specials. + $matches = null; + $have_specials = preg_match_all( + '/^\s*@(\w+)\s*([^\n]*)/m', + $docblock, + $matches, + PREG_SET_ORDER); + if ($have_specials) { + $docblock = preg_replace('/^\s*@(\w+)\s*([^\n]*)/m', '', $docblock); + foreach ($matches as $match) { + list($_, $type, $data) = $match; + $data = trim($data); + if (isset($special[$type])) { + $special[$type] = $special[$type]."\n".$data; + } else { + $special[$type] = $data; + } + } + } + + $docblock = str_replace("\t", ' ', $docblock); + + // Smush the whole docblock to the left edge. + $min_indent = 80; + $indent = 0; + foreach (array_filter(explode("\n", $docblock)) as $line) { + for ($ii = 0; $ii < strlen($line); $ii++) { + if ($line[$ii] != ' ') { + break; + } + $indent++; + } + $min_indent = min($indent, $min_indent); + } + + $docblock = preg_replace( + '/^'.str_repeat(' ', $min_indent).'/m', + '', + $docblock); + $docblock = rtrim($docblock); + // Trim any empty lines off the front, but leave the indent level if there + // is one. + $docblock = preg_replace('/^\s*\n/', '', $docblock); + + return array($docblock, $special); + } +} diff --git a/src/parser/docblock/__init__.php b/src/parser/docblock/__init__.php new file mode 100644 index 00000000..098ed673 --- /dev/null +++ b/src/parser/docblock/__init__.php @@ -0,0 +1,10 @@ +parseDocblock($root.$file); + } + } + + private function parseDocblock($doc_file) { + $contents = Filesystem::readFile($doc_file); + $file = basename($doc_file); + + $parser = new PhutilDocblockParser(); + list($docblock, $specials) = $parser->parse($contents); + + switch ($file) { + case 'embedded-specials.docblock': + $this->assertEqual(array(), $specials); + $this->assertEqual( + "So long as a @special does not appear at the beginning of a line,\n". + "it is parsed as normal text.", + $docblock); + break; + case 'indented-block.docblock': + $this->assertEqual(array(), $specials); + $this->assertEqual( + "Cozy lummox gives smart squid who asks for job pen.", + $docblock); + break; + case 'indented-text.docblock': + $this->assertEqual(array(), $specials); + $this->assertEqual( + "Cozy lummox gives smart squid who asks for job pen.", + $docblock); + break; + case 'multi-specials.docblock': + $this->assertEqual( + array( + 'special' => "north\nsouth", + ), + $specials); + $this->assertEqual( + "", + $docblock); + break; + case 'specials.docblock': + $this->assertEqual( + array( + 'type' => 'type', + 'task' => 'task', + ), + $specials); + $this->assertEqual( + "", + $docblock); + break; + case 'linebreak-breaks-specials.docblock': + $this->assertEqual( + array( + 'title' => 'title', + ), + $specials); + $this->assertEqual( + "This is normal text, not part of the @title.", + $docblock); + break; + default: + throw new Exception("No test case to handle file '{$file}'!"); + } + } +} diff --git a/src/parser/docblock/__tests__/__init__.php b/src/parser/docblock/__tests__/__init__.php new file mode 100644 index 00000000..5da736bf --- /dev/null +++ b/src/parser/docblock/__tests__/__init__.php @@ -0,0 +1,15 @@ +