diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 2fefa1a..c54a188 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,412 +1,416 @@
2,
'class' =>
array(
'AASTNode' => 'parser/aast/api/AASTNode.php',
'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
'AASTToken' => 'parser/aast/api/AASTToken.php',
'AASTTree' => 'parser/aast/api/AASTTree.php',
'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
'AphrontMySQLDatabaseConnectionBase' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnectionBase.php',
'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
'AphrontQueryAccessDeniedException' => 'aphront/storage/exception/AphrontQueryAccessDeniedException.php',
'AphrontQueryConnectionException' => 'aphront/storage/exception/AphrontQueryConnectionException.php',
'AphrontQueryConnectionLostException' => 'aphront/storage/exception/AphrontQueryConnectionLostException.php',
'AphrontQueryCountException' => 'aphront/storage/exception/AphrontQueryCountException.php',
'AphrontQueryDeadlockException' => 'aphront/storage/exception/AphrontQueryDeadlockException.php',
'AphrontQueryDuplicateKeyException' => 'aphront/storage/exception/AphrontQueryDuplicateKeyException.php',
'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
'AphrontQueryNotSupportedException' => 'aphront/storage/exception/AphrontQueryNotSupportedException.php',
'AphrontQueryObjectMissingException' => 'aphront/storage/exception/AphrontQueryObjectMissingException.php',
'AphrontQueryParameterException' => 'aphront/storage/exception/AphrontQueryParameterException.php',
'AphrontQueryRecoverableException' => 'aphront/storage/exception/AphrontQueryRecoverableException.php',
'AphrontQuerySchemaException' => 'aphront/storage/exception/AphrontQuerySchemaException.php',
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
'CommandException' => 'future/CommandException.php',
'ConduitClient' => 'conduit/ConduitClient.php',
'ConduitClientException' => 'conduit/ConduitClientException.php',
'ConduitFuture' => 'conduit/ConduitFuture.php',
'ExecFuture' => 'future/ExecFuture.php',
'ExecFutureTestCase' => 'future/__tests__/ExecFutureTestCase.php',
'FileFinder' => 'filesystem/FileFinder.php',
'FileList' => 'filesystem/FileList.php',
'Filesystem' => 'filesystem/Filesystem.php',
'FilesystemException' => 'filesystem/FilesystemException.php',
'Future' => 'future/Future.php',
'FutureIterator' => 'future/FutureIterator.php',
'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
'FutureProxy' => 'future/FutureProxy.php',
'HTTPFuture' => 'future/http/HTTPFuture.php',
'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
'HTTPFutureResponseStatusCURL' => 'future/http/status/HTTPFutureResponseStatusCURL.php',
'HTTPFutureResponseStatusHTTP' => 'future/http/status/HTTPFutureResponseStatusHTTP.php',
'HTTPFutureResponseStatusParse' => 'future/http/status/HTTPFutureResponseStatusParse.php',
'HTTPFutureResponseStatusTransport' => 'future/http/status/HTTPFutureResponseStatusTransport.php',
'HTTPSFuture' => 'future/http/HTTPSFuture.php',
'ImmediateFuture' => 'future/ImmediateFuture.php',
'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
'PhutilAggregateException' => 'error/PhutilAggregateException.php',
'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
'PhutilBufferedIteratorExample' => 'utils/PhutilBufferedIteratorExample.php',
'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
'PhutilChannel' => 'channel/PhutilChannel.php',
'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
'PhutilConsole' => 'console/PhutilConsole.php',
'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
'PhutilDaemon' => 'daemon/PhutilDaemon.php',
'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php',
'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php',
'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php',
'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php',
'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
'PhutilEvent' => 'events/PhutilEvent.php',
'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
'PhutilEventEngine' => 'events/PhutilEventEngine.php',
'PhutilEventListener' => 'events/PhutilEventListener.php',
'PhutilEventType' => 'events/constant/PhutilEventType.php',
'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
'PhutilGitURI' => 'parser/PhutilGitURI.php',
'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
'PhutilJSON' => 'parser/PhutilJSON.php',
'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
'PhutilLexer' => 'lexer/PhutilLexer.php',
+ 'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
'PhutilLock' => 'filesystem/PhutilLock.php',
'PhutilLockException' => 'filesystem/PhutilLockException.php',
'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
+ 'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php',
'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
'PhutilPHTTestCase' => 'internationalization/__tests__/PhutilPHTTestCase.php',
'PhutilPerson' => 'internationalization/PhutilPerson.php',
'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
'PhutilRemarkupEngineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineBlockRule.php',
'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupCodeBlockRule.php',
'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupDefaultBlockRule.php',
'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupHeaderBlockRule.php',
'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupInlineBlockRule.php',
'PhutilRemarkupEngineRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupListBlockRule.php',
'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupLiteralBlockRule.php',
'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupNoteBlockRule.php',
'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupQuotesBlockRule.php',
'PhutilRemarkupEngineRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupEngineRemarkupTableBlockRule.php',
'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleBold.php',
'PhutilRemarkupRuleDel' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleDel.php',
'PhutilRemarkupRuleDocumentLink' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleDocumentLink.php',
'PhutilRemarkupRuleEscapeHTML' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeHTML.php',
'PhutilRemarkupRuleEscapeRemarkup' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleEscapeRemarkup.php',
'PhutilRemarkupRuleHyperlink' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleHyperlink.php',
'PhutilRemarkupRuleItalic' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleItalic.php',
'PhutilRemarkupRuleLinebreaks' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleLinebreaks.php',
'PhutilRemarkupRuleMonospace' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRuleMonospace.php',
'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php',
'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
'PhutilSprite' => 'sprites/PhutilSprite.php',
'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php',
'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php',
'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php',
'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
'PhutilURI' => 'parser/PhutilURI.php',
'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
'TempFile' => 'filesystem/TempFile.php',
'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
),
'function' =>
array(
'Futures' => 'future/functions.php',
'_qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
'_qsprintf_check_type' => 'xsprintf/qsprintf.php',
'array_mergev' => 'utils/utils.php',
'array_select_keys' => 'utils/utils.php',
'assert_instances_of' => 'utils/utils.php',
'coalesce' => 'utils/utils.php',
'csprintf' => 'xsprintf/csprintf.php',
'exec_manual' => 'future/execx.php',
'execx' => 'future/execx.php',
'head' => 'utils/utils.php',
'head_key' => 'utils/utils.php',
'hsprintf' => 'markup/render.php',
'id' => 'utils/utils.php',
'idx' => 'utils/utils.php',
'ifilter' => 'utils/utils.php',
'igroup' => 'utils/utils.php',
'ipull' => 'utils/utils.php',
'isort' => 'utils/utils.php',
'jsprintf' => 'xsprintf/jsprintf.php',
'last' => 'utils/utils.php',
'last_key' => 'utils/utils.php',
'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
'mfilter' => 'utils/utils.php',
'mgroup' => 'utils/utils.php',
'mpull' => 'utils/utils.php',
'msort' => 'utils/utils.php',
'newv' => 'utils/utils.php',
'nonempty' => 'utils/utils.php',
'phlog' => 'error/phlog.php',
'pht' => 'internationalization/pht.php',
'phutil_console_confirm' => 'console/format.php',
'phutil_console_format' => 'console/format.php',
'phutil_console_prompt' => 'console/format.php',
'phutil_console_require_tty' => 'console/format.php',
'phutil_console_wrap' => 'console/format.php',
'phutil_deprecated' => 'moduleutils/moduleutils.php',
'phutil_error_listener_example' => 'error/phlog.php',
'phutil_escape_html' => 'markup/render.php',
'phutil_escape_uri' => 'markup/render.php',
'phutil_escape_uri_path_component' => 'markup/render.php',
'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
'phutil_is_utf8' => 'utils/utf8.php',
'phutil_passthru' => 'future/execx.php',
'phutil_render_tag' => 'markup/render.php',
'phutil_unescape_uri_path_component' => 'markup/render.php',
'phutil_utf8_convert' => 'utils/utf8.php',
'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
'phutil_utf8_shorten' => 'utils/utf8.php',
'phutil_utf8_strlen' => 'utils/utf8.php',
'phutil_utf8ize' => 'utils/utf8.php',
'phutil_utf8v' => 'utils/utf8.php',
'qsprintf' => 'xsprintf/qsprintf.php',
'queryfx' => 'xsprintf/queryfx.php',
'queryfx_all' => 'xsprintf/queryfx.php',
'queryfx_one' => 'xsprintf/queryfx.php',
'vcsprintf' => 'xsprintf/csprintf.php',
'vjsprintf' => 'xsprintf/jsprintf.php',
'vqsprintf' => 'xsprintf/qsprintf.php',
'vqueryfx' => 'xsprintf/queryfx.php',
'vqueryfx_all' => 'xsprintf/queryfx.php',
'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
'xhpast_get_binary_path' => 'parser/xhpast/bin/xhpast_parse.php',
'xhpast_get_build_instructions' => 'parser/xhpast/bin/xhpast_parse.php',
'xhpast_get_parser_future' => 'parser/xhpast/bin/xhpast_parse.php',
'xhpast_is_available' => 'parser/xhpast/bin/xhpast_parse.php',
'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
'xsprintf' => 'xsprintf/xsprintf.php',
'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
'xsprintf_command' => 'xsprintf/csprintf.php',
'xsprintf_javascript' => 'xsprintf/jsprintf.php',
'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
'xsprintf_query' => 'xsprintf/qsprintf.php',
),
'xmap' =>
array(
'AASTNodeList' =>
array(
0 => 'Iterator',
1 => 'Countable',
),
'AbstractDirectedGraphTestCase' => 'ArcanistPhutilTestCase',
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontMySQLDatabaseConnection' => 'AphrontMySQLDatabaseConnectionBase',
'AphrontMySQLDatabaseConnectionBase' => 'AphrontDatabaseConnection',
'AphrontMySQLiDatabaseConnection' => 'AphrontMySQLDatabaseConnectionBase',
'AphrontQueryAccessDeniedException' => 'AphrontQueryRecoverableException',
'AphrontQueryConnectionException' => 'AphrontQueryException',
'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
'AphrontQueryCountException' => 'AphrontQueryException',
'AphrontQueryDeadlockException' => 'AphrontQueryRecoverableException',
'AphrontQueryDuplicateKeyException' => 'AphrontQueryException',
'AphrontQueryException' => 'Exception',
'AphrontQueryNotSupportedException' => 'AphrontQueryException',
'AphrontQueryObjectMissingException' => 'AphrontQueryException',
'AphrontQueryParameterException' => 'AphrontQueryException',
'AphrontQueryRecoverableException' => 'AphrontQueryException',
'AphrontQuerySchemaException' => 'AphrontQueryException',
'BaseHTTPFuture' => 'Future',
'CommandException' => 'Exception',
'ConduitClientException' => 'Exception',
'ConduitFuture' => 'FutureProxy',
'ExecFuture' => 'Future',
'ExecFutureTestCase' => 'ArcanistPhutilTestCase',
'FilesystemException' => 'Exception',
'FutureIterator' => 'Iterator',
'FutureIteratorTestCase' => 'ArcanistPhutilTestCase',
'FutureProxy' => 'Future',
'HTTPFuture' => 'BaseHTTPFuture',
'HTTPFutureResponseStatus' => 'Exception',
'HTTPFutureResponseStatusCURL' => 'HTTPFutureResponseStatus',
'HTTPFutureResponseStatusHTTP' => 'HTTPFutureResponseStatus',
'HTTPFutureResponseStatusParse' => 'HTTPFutureResponseStatus',
'HTTPFutureResponseStatusTransport' => 'HTTPFutureResponseStatus',
'HTTPSFuture' => 'BaseHTTPFuture',
'ImmediateFuture' => 'Future',
'LinesOfALarge' => 'Iterator',
'LinesOfALargeExecFuture' => 'LinesOfALarge',
'LinesOfALargeExecFutureTestCase' => 'ArcanistPhutilTestCase',
'LinesOfALargeFile' => 'LinesOfALarge',
'LinesOfALargeFileTestCase' => 'ArcanistPhutilTestCase',
'PhutilAWSEC2Future' => 'PhutilAWSFuture',
'PhutilAWSException' => 'Exception',
'PhutilAWSFuture' => 'FutureProxy',
'PhutilAggregateException' => 'Exception',
'PhutilArgumentParserException' => 'Exception',
'PhutilArgumentParserTestCase' => 'ArcanistPhutilTestCase',
'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
'PhutilArgumentSpecificationTestCase' => 'ArcanistPhutilTestCase',
'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
'PhutilBufferedIterator' => 'Iterator',
'PhutilBufferedIteratorExample' => 'PhutilBufferedIterator',
'PhutilBufferedIteratorTestCase' => 'ArcanistPhutilTestCase',
'PhutilChannelChannel' => 'PhutilChannel',
'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
'PhutilConsoleStdinNotInteractiveException' => 'Exception',
'PhutilConsoleWrapTestCase' => 'ArcanistPhutilTestCase',
'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'ArcanistPhutilTestCase',
'PhutilDeferredLogTestCase' => 'ArcanistPhutilTestCase',
'PhutilDocblockParserTestCase' => 'ArcanistPhutilTestCase',
'PhutilEmailAddressTestCase' => 'ArcanistPhutilTestCase',
'PhutilEventType' => 'PhutilEventConstants',
'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
'PhutilExecChannel' => 'PhutilChannel',
'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
'PhutilFileLock' => 'PhutilLock',
'PhutilFileLockTestCase' => 'ArcanistPhutilTestCase',
'PhutilGitURITestCase' => 'ArcanistPhutilTestCase',
'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
'PhutilJSONProtocolChannelTestCase' => 'ArcanistPhutilTestCase',
'PhutilJSONTestCase' => 'ArcanistPhutilTestCase',
'PhutilLanguageGuesserTestCase' => 'ArcanistPhutilTestCase',
+ 'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
'PhutilLockException' => 'Exception',
'PhutilMarkupTestCase' => 'ArcanistPhutilTestCase',
'PhutilMissingSymbolException' => 'Exception',
'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
'PhutilOpaqueEnvelopeTestCase' => 'ArcanistPhutilTestCase',
'PhutilPHPFragmentLexer' => 'PhutilLexer',
+ 'PhutilPHPFragmentLexerHighlighterTestCase' => 'ArcanistPhutilTestCase',
'PhutilPHPFragmentLexerTestCase' => 'ArcanistPhutilTestCase',
'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
'PhutilPHPObjectProtocolChannelTestCase' => 'ArcanistPhutilTestCase',
'PhutilPHTTestCase' => 'ArcanistPhutilTestCase',
'PhutilPersonTest' => 'PhutilPerson',
'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
'PhutilProtocolChannel' => 'PhutilChannelChannel',
'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineRemarkupTableBlockRule' => 'PhutilRemarkupEngineBlockRule',
'PhutilRemarkupEngineTestCase' => 'ArcanistPhutilTestCase',
'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleDel' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleDocumentLink' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule',
'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule',
'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
'PhutilSimpleOptionsTestCase' => 'ArcanistPhutilTestCase',
'PhutilSocketChannel' => 'PhutilChannel',
'PhutilSyntaxHighlighterException' => 'Exception',
'PhutilTortureTestDaemon' => 'PhutilDaemon',
'PhutilTranslatorTestCase' => 'ArcanistPhutilTestCase',
'PhutilURITestCase' => 'ArcanistPhutilTestCase',
'PhutilUTF8TestCase' => 'ArcanistPhutilTestCase',
'PhutilUtilsTestCase' => 'ArcanistPhutilTestCase',
'PhutilXHPASTSyntaxHighlighterTestCase' => 'ArcanistPhutilTestCase',
'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
'XHPASTNode' => 'AASTNode',
'XHPASTSyntaxErrorException' => 'Exception',
'XHPASTToken' => 'AASTToken',
'XHPASTTree' => 'AASTTree',
'XHPASTTreeTestCase' => 'ArcanistPhutilTestCase',
),
));
diff --git a/src/lexer/PhutilPHPFragmentLexer.php b/src/lexer/PhutilPHPFragmentLexer.php
index bfd771a..f85361a 100644
--- a/src/lexer/PhutilPHPFragmentLexer.php
+++ b/src/lexer/PhutilPHPFragmentLexer.php
@@ -1,304 +1,304 @@
array(
array('<\\?(php)?', 'cp', 'php'),
array('[^<]+', null),
array('<', null),
),
'php' => array_merge(array(
array('\\?>', 'cp', '!pop'),
array(
'<<<([\'"]?)('.$identifier_pattern.')\\1\\n.*?\\n\\2\\;?\\n',
's'),
), $nonsemantic_rules, array(
array('__halt_compiler\\b', 'cp', 'halt_compiler'),
array('(->|::)', 'o', 'attr'),
array('[~!%^&*+=|:.<>/?@-]+', 'o'),
array('[\\[\\]{}();,]', 'o'),
// After 'new', try to match an unadorned symbol.
array('(new|instanceof)\\b', 'k', 'possible_classname',
array(
'case-insensitive' => true,
),
),
array('function\\b', 'k', 'function_definition',
array(
'case-insensitive' => true,
),
),
// After 'extends' or 'implements', match a list of classes/interfaces.
array('(extends|implements)\\b', 'k', 'class_list',
array(
'case-insensitive' => true,
),
),
array('catch\\b', 'k', 'catch',
array(
'case-insensitive' => true,
),
),
array('('.implode('|', $keywords).')\\b', 'k', null,
array(
'case-insensitive' => true,
)),
array('('.implode('|', $constants).')\\b', 'kc', null,
array(
'case-insensitive' => true,
)),
array('\\$+'.$identifier_pattern, 'nv'),
// Match "f(" as a function and "C::" as a class. These won't work
// if you put a comment between the symbol and the operator, but
// that's a bizarre usage.
array($identifier_ns_pattern.'(?=\s*[\\(])', 'nf'),
array($identifier_ns_pattern.'(?=\s*::)', 'nc', 'context_attr',
array(
'context' => 'push',
),
),
array($identifier_ns_pattern, 'no'),
array('(\\d+\\.\\d*|\\d*\\.\\d+)([eE][+-]?[0-9]+)?', 'mf'),
array('\\d+[eE][+-]?[0-9]+', 'mf'),
array('0[0-7]+', 'mo'),
array('0[xX][a-fA-F0-9]+', 'mh'),
array('0[bB][0-1]+', 'm'),
array('\d+', 'mi'),
array("'", "s1", 'string1'),
array("`", "sb", 'stringb'),
array('"', 's2', 'string2'),
array('.', null),
)),
// We've just matched a class name, with a "::" lookahead. The name of
// the class is on the top of the context stack. We want to try to match
// the attribute or method (e.g., "X::C" or "X::f()").
'context_attr' => array_merge($nonsemantic_rules, array(
array('::', 'o'),
array($identifier_pattern.'(?=\s*[\\(])', 'nf', '!pop',
array(
'context' => 'pop',
),
),
array($identifier_pattern, 'na', '!pop',
array(
'context' => 'pop',
),
),
array('', null, '!pop',
array(
'context' => 'discard',
),
),
)),
// After '->' or '::', a symbol is an attribute name. Note that we end
// up in 'context_attr' instead of here in some cases.
'attr' => array_merge($nonsemantic_rules, array(
array($identifier_pattern, 'na', '!pop'),
array('', null, '!pop'),
)),
// After 'new', a symbol is a class name.
'possible_classname' => array_merge($nonsemantic_rules, array(
array($identifier_ns_pattern, 'nc', '!pop'),
array('', null, '!pop'),
)),
'string1' => array(
array('[^\'\\\\]+', 's1'),
array("'", 's1', '!pop'),
array('\\\\.', 'k'),
),
'stringb' => array(
array('[^`\\\\]+', 'sb'),
array('`', 'sb', '!pop'),
array('\\\\.', 'k'),
),
'string2' => array(
array('[^"\\\\]+', 's2'),
array('"', 's2', '!pop'),
array('\\\\.', 'k'),
),
// In a function definition (after "function"), we don't link the name
// as a "nf" (name.function) since it is its own definition.
'function_definition' => array_merge($nonsemantic_rules, array(
array('&', 'o'),
array('\\(', 'o', '!pop'),
array('[a-zA-Z_][a-zA-Z0-9_]*', 'no', '!pop'),
)),
// For "//" and "#" comments, we need to break out if we see "?" followed
// by ">".
'line_comment' => array(
array('[^?\\n]+', 'c'),
array('\\n', null, '!pop'),
array('(?=\\?>)', null, '!pop'),
- array('\\?', 'c', '!pop'),
+ array('\\?', 'c'),
),
// We've seen __halt_compiler. Grab the '();' afterward and then eat
// the rest of the file as raw data.
'halt_compiler' => array_merge($nonsemantic_rules, array(
array('[()]', 'o'),
array(';', 'o', 'compiler_halted'),
array('\\?>', 'o', 'compiler_halted'),
// Just halt on anything else.
array('', null, 'compiler_halted'),
)),
// __halt_compiler has taken effect.
'compiler_halted' => array(
array('.+', null),
),
'class_list' => array_merge($nonsemantic_rules, array(
array(',', 'o'),
array('implements', 'k'),
array($identifier_ns_pattern, 'nc'),
array('', null, '!pop'),
)),
'catch' => array_merge($nonsemantic_rules, array(
array('\\(', 'o'),
array($identifier_ns_pattern, 'nc'),
array('', null, '!pop'),
)),
);
}
}
diff --git a/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php b/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php
index e38213b..6812fae 100644
--- a/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php
+++ b/src/markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php
@@ -1,100 +1,106 @@
config[$key] = $value;
return $this;
}
public function getLanguageFromFilename($filename) {
static $default_map = array(
// All files which have file extensions that we haven't already matched
// map to their extensions.
'@\\.([^./]+)$@' => 1,
);
$maps = array();
if (!empty($this->config['filename.map'])) {
$maps[] = $this->config['filename.map'];
}
$maps[] = $default_map;
foreach ($maps as $map) {
foreach ($map as $regexp => $lang) {
$matches = null;
if (preg_match($regexp, $filename, $matches)) {
if (is_numeric($lang)) {
return idx($matches, $lang);
} else {
return $lang;
}
}
}
}
return null;
}
public function getHighlightFuture($language, $source) {
if ($language === null) {
$language = PhutilLanguageGuesser::guessLanguage($source);
}
$have_pygments = !empty($this->config['pygments.enabled']);
if ($language == 'php' && xhpast_is_available()) {
return id(new PhutilXHPASTSyntaxHighlighter())
- ->setConfig('pygments.enabled', $have_pygments)
->getHighlightFuture($source);
}
if ($language == 'console') {
return id(new PhutilConsoleSyntaxHighlighter())
->getHighlightFuture($source);
}
if ($language == 'diviner' || $language == 'remarkup') {
return id(new PhutilDivinerSyntaxHighlighter())
->getHighlightFuture($source);
}
if ($language == 'rainbow') {
return id(new PhutilRainbowSyntaxHighlighter())
->getHighlightFuture($source);
}
+ if ($language == 'php') {
+ return id(new PhutilLexerSyntaxHighlighter())
+ ->setConfig('lexer', new PhutilPHPFragmentLexer())
+ ->setConfig('language', 'php')
+ ->getHighlightFuture($source);
+ }
+
if ($have_pygments) {
return id(new PhutilPygmentsSyntaxHighlighter())
->setConfig('language', $language)
->getHighlightFuture($source);
}
return id(new PhutilDefaultSyntaxHighlighter())
->getHighlightFuture($source);
}
}
diff --git a/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php
new file mode 100644
index 0000000..58a952b
--- /dev/null
+++ b/src/markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php
@@ -0,0 +1,99 @@
+config[$key] = $value;
+ return $this;
+ }
+
+ public function getHighlightFuture($source) {
+ $strip = false;
+ $state = 'start';
+ $lang = idx($this->config, 'language');
+
+ if ($lang == 'php') {
+ if (strpos($source, '') === false) {
+ $state = 'php';
+ }
+ }
+
+ $lexer = idx($this->config, 'lexer');
+ $tokens = $lexer->getTokens($source, $state);
+ $tokens = $lexer->mergeTokens($tokens);
+
+ $result = array();
+ foreach ($tokens as $token) {
+ list($type, $value, $context) = $token;
+ $value = phutil_escape_html($value);
+
+ $data_name = null;
+ switch ($type) {
+ case 'nc':
+ case 'nf':
+ case 'na':
+ $data_name = ' data-symbol-name="'.$value.'"';
+ break;
+ }
+
+ $data_context = null;
+ if ($context) {
+ $context = phutil_escape_html($context);
+ $data_context = ' data-symbol-context="'.$context.'"';
+ }
+
+ $class_name = null;
+ if ($type) {
+ $class_name = ' class="'.$type.'"';
+ }
+
+ if (strpos($value, "\n") !== false) {
+ $value = explode("\n", $value);
+ } else {
+ $value = array($value);
+ }
+ foreach ($value as $part) {
+ if (strlen($part)) {
+ if ($class_name) {
+ $result[] =
+ ''.
+ $part.
+ '';
+ } else {
+ $result[] = $part;
+ }
+ }
+ $result[] = "\n";
+ }
+
+ // Throw away the last "\n".
+ array_pop($result);
+ }
+
+ $result = implode('', $result);
+
+ return new ImmediateFuture($result);
+ }
+
+}
diff --git a/src/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php b/src/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php
index 276922d..bf5d8ed 100644
--- a/src/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php
+++ b/src/markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php
@@ -1,257 +1,252 @@
config[$key] = $value;
return $this;
}
public function getHighlightFuture($source) {
try {
$result = $this->applyXHPHighlight($source);
return new ImmediateFuture($result);
} catch (Exception $ex) {
- if (!empty($this->config['pygments.enabled'])) {
- // Fall back to Pygments if we failed to highlight using XHP. The XHP
- // highlighter currently uses a parser, not just a lexer, so it fails on
- // snippets which aren't valid syntactically.
- return id(new PhutilPygmentsSyntaxHighlighter())
- ->setConfig('language', 'php')
- ->getHighlightFuture($source);
- } else {
- return id(new PhutilDefaultSyntaxHighlighter())
- ->getHighlightFuture($source);
- }
+ // XHP can't highlight source that isn't syntactically valid. Fall back
+ // to the fragment lexer.
+ return id(new PhutilLexerSyntaxHighlighter())
+ ->setConfig('lexer', new PhutilPHPFragmentLexer())
+ ->setConfig('language', 'php')
+ ->getHighlightFuture($source);
}
}
private function applyXHPHighlight($source) {
// We perform two passes here: one using the AST to find symbols we care
// about -- particularly, class names and function names. These are used
// in the crossreference stuff to link into Diffusion. After we've done our
// AST pass, we do a followup pass on the token stream to catch all the
// simple stuff like strings and comments.
$scrub = false;
if (strpos($source, '') === false) {
$source = "getRootNode();
$tokens = $root->getTokens();
$interesting_symbols = $this->findInterestingSymbols($root);
$out = array();
foreach ($tokens as $key => $token) {
$value = phutil_escape_html($token->getValue());
$class = null;
$multi = false;
$attrs = '';
if (isset($interesting_symbols[$key])) {
$sym = $interesting_symbols[$key];
$class = $sym[0];
if (isset($sym['context'])) {
$attrs = $attrs.' data-symbol-context="'.$sym['context'].'"';
}
if (isset($sym['symbol'])) {
$attrs = $attrs.' data-symbol-name="'.$sym['symbol'].'"';
}
} else {
switch ($token->getTypeName()) {
case 'T_WHITESPACE':
break;
case 'T_DOC_COMMENT':
$class = 'dc';
$multi = true;
break;
case 'T_COMMENT':
$class = 'c';
$multi = true;
break;
case 'T_CONSTANT_ENCAPSED_STRING':
case 'T_ENCAPSED_AND_WHITESPACE':
case 'T_INLINE_HTML':
$class = 's';
$multi = true;
break;
case 'T_VARIABLE':
$class = 'nv';
break;
case 'T_OPEN_TAG':
case 'T_OPEN_TAG_WITH_ECHO':
case 'T_CLOSE_TAG':
$class = 'o';
break;
case 'T_LNUMBER':
case 'T_DNUMBER':
$class = 'm';
break;
case 'T_STRING':
static $magic = array(
'true' => true,
'false' => true,
'null' => true,
);
if (isset($magic[$value])) {
$class = 'k';
break;
}
$class = 'nx';
break;
default:
$class = 'k';
break;
}
}
if ($class) {
$l = '';
$r = '';
$value = $l.$value.$r;
if ($multi) {
// If the token may have multiple lines in it, make sure each
// crosses no more than one line so the lines can be put
// in a table, etc., later.
$value = str_replace(
"\n",
$r."\n".$l,
$value);
}
$out[] = $value;
} else {
$out[] = $value;
}
}
if ($scrub) {
array_shift($out);
}
return rtrim(implode('', $out));
}
private function findInterestingSymbols(XHPASTNode $root) {
// Class name symbols appear in:
// class X extends X implements X, X { ... }
// new X();
// $x instanceof X
// catch (X $x)
// function f(X $x)
// X::f();
// X::$m;
// X::CONST;
// These are PHP builtin tokens which can appear in a classname context.
// Don't link them since they don't go anywhere useful.
static $builtin_class_tokens = array(
'self' => true,
'parent' => true,
'static' => true,
);
// Fortunately XHPAST puts all of these in a special node type so it's
// easy to find them.
$result_map = array();
$class_names = $root->selectDescendantsOfType('n_CLASS_NAME');
foreach ($class_names as $class_name) {
foreach ($class_name->getTokens() as $key => $token) {
if (isset($builtin_class_tokens[$token->getValue()])) {
// This is something like "self::method()".
continue;
}
$result_map[$key] = array(
'nc', // "Name, Class"
'symbol' => $class_name->getConcreteString(),
);
}
}
// Function name symbols appear in:
// f()
$function_calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
foreach ($function_calls as $call) {
$call = $call->getChildByIndex(0);
if ($call->getTypeName() == 'n_SYMBOL_NAME') {
// This is a normal function call, not some $f() shenanigans.
foreach ($call->getTokens() as $key => $token) {
$result_map[$key] = array(
'nf', // "Name, Function"
'symbol' => $call->getConcreteString(),
);
}
}
}
// Upon encountering $x->y, link y without context, since $x is unknown.
$prop_access = $root->selectDescendantsOfType('n_OBJECT_PROPERTY_ACCESS');
foreach ($prop_access as $access) {
$right = $access->getChildByIndex(1);
if ($right->getTypeName() == 'n_INDEX_ACCESS') {
// otherwise $x->y[0] doesn't get highlighted
$right = $right->getChildByIndex(0);
}
if ($right->getTypeName() == 'n_STRING') {
foreach ($right->getTokens() as $key => $token) {
$result_map[$key] = array(
'na', // "Name, Attribute"
'symbol' => $right->getConcreteString(),
);
}
}
}
// Upon encountering x::y, try to link y with context x.
$static_access = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
foreach ($static_access as $access) {
$class = $access->getChildByIndex(0);
$right = $access->getChildByIndex(1);
if ($class->getTypeName() == 'n_CLASS_NAME' &&
($right->getTypeName() == 'n_STRING' ||
$right->getTypeName() == 'n_VARIABLE')) {
$classname = head($class->getTokens())->getValue();
$result = array(
'na',
'symbol' => ltrim($right->getConcreteString(), '$'),
);
if (!isset($builtin_class_tokens[$classname])) {
$result['context'] = $classname;
}
foreach ($right->getTokens() as $key => $token) {
$result_map[$key] = $result;
}
}
}
return $result_map;
}
}
diff --git a/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php b/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php
new file mode 100644
index 0000000..1e7fd84
--- /dev/null
+++ b/src/markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php
@@ -0,0 +1,45 @@
+setConfig('language', 'php');
+ $highlighter->setConfig('lexer', new PhutilPHPFragmentLexer());
+
+
+ $path = dirname(__FILE__).'/phpfragment/';
+ foreach (Filesystem::listDirectory($path, $include_hidden = false) as $f) {
+ if (preg_match('/.test$/', $f)) {
+ $expect = preg_replace('/.test$/', '.expect', $f);
+ $source = Filesystem::readFile($path.'/'.$f);
+
+ $this->assertEqual(
+ Filesystem::readFile($path.'/'.$expect),
+ $highlighter->getHighlightFuture($source)->resolve(),
+ $f);
+ }
+ }
+ }
+
+}
diff --git a/src/markup/syntax/highlighter/__tests__/phpfragment/abuse.expect b/src/markup/syntax/highlighter/__tests__/phpfragment/abuse.expect
new file mode 100644
index 0000000..aa31ec0
--- /dev/null
+++ b/src/markup/syntax/highlighter/__tests__/phpfragment/abuse.expect
@@ -0,0 +1,16 @@
+<?
+
+// comment? comment! ?>
+
+data
+
+<?php
+
+__halt_compiler /* ! */ ( // )
+) /* ;;;; */
+
+;
+
+data data
+<?php
+data
\ No newline at end of file
diff --git a/src/markup/syntax/highlighter/__tests__/phpfragment/abuse.test b/src/markup/syntax/highlighter/__tests__/phpfragment/abuse.test
new file mode 100644
index 0000000..b3ade94
--- /dev/null
+++ b/src/markup/syntax/highlighter/__tests__/phpfragment/abuse.test
@@ -0,0 +1,16 @@
+
+
+// comment? comment! ?>
+
+data
+
+public function f() {
+ ExampleClass::EXAMPLE_CONSTANT;
+ ExampleClass::exampleMethod();
+ example_function();
+}
diff --git a/src/markup/syntax/highlighter/__tests__/phpfragment/basics.test b/src/markup/syntax/highlighter/__tests__/phpfragment/basics.test
new file mode 100644
index 0000000..87e3f9a
--- /dev/null
+++ b/src/markup/syntax/highlighter/__tests__/phpfragment/basics.test
@@ -0,0 +1,5 @@
+public function f() {
+ ExampleClass::EXAMPLE_CONSTANT;
+ ExampleClass::exampleMethod();
+ example_function();
+}