diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7ac49dd..508c4c5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,261 +1,262 @@ array( 'AASTNode' => 'parser/aast/api/node', 'AASTNodeList' => 'parser/aast/api/list', 'AASTToken' => 'parser/aast/api/token', 'AASTTree' => 'parser/aast/api/tree', 'AbstractDirectedGraph' => 'utils/abstractgraph', 'AbstractDirectedGraphTestCase' => 'utils/abstractgraph/__tests__', 'BaseHTTPFuture' => 'future/http/base', 'CommandException' => 'future/exec', 'ConduitClient' => 'conduit/client', 'ConduitClientException' => 'conduit/client', 'ConduitFuture' => 'conduit/client', 'ExecFuture' => 'future/exec', 'ExecFutureTestCase' => 'future/exec/__tests__', 'FileFinder' => 'filesystem/filefinder', 'FileList' => 'filesystem/filelist', 'Filesystem' => 'filesystem', 'FilesystemException' => 'filesystem', 'Future' => 'future', 'FutureIterator' => 'future', 'FutureProxy' => 'future/proxy', 'HTTPFuture' => 'future/http/http', 'HTTPFutureResponseStatus' => 'future/http/status/base', 'HTTPFutureResponseStatusCURL' => 'future/http/status/curl', 'HTTPFutureResponseStatusHTTP' => 'future/http/status/http', 'HTTPFutureResponseStatusParse' => 'future/http/status/parse', 'HTTPFutureResponseStatusTransport' => 'future/http/status/transport', 'HTTPSFuture' => 'future/http/https', 'ImmediateFuture' => 'future/immediate', 'LinesOfALargeFile' => 'filesystem/linesofalargefile', 'MFilterTestHelper' => 'utils/__tests__', 'PhutilAWSEC2Future' => 'future/aws/ec2', 'PhutilAWSException' => 'future/aws/exception', 'PhutilAWSFuture' => 'future/aws/base', 'PhutilArgumentParser' => 'parser/argument/parser', 'PhutilArgumentParserException' => 'parser/argument/exception/base', 'PhutilArgumentParserTestCase' => 'parser/argument/parser/__tests__', 'PhutilArgumentSpecification' => 'parser/argument/spec', 'PhutilArgumentSpecificationException' => 'parser/argument/exception/specification', 'PhutilArgumentSpecificationTestCase' => 'parser/argument/spec/__tests__', 'PhutilArgumentUsageException' => 'parser/argument/exception/usage', 'PhutilConsoleFormatter' => 'console', 'PhutilConsoleStdinNotInteractiveException' => 'console/exception', 'PhutilConsoleWrapTestCase' => 'console/__tests__', 'PhutilDaemon' => 'daemon/base', 'PhutilDaemonOverseer' => 'daemon/overseer', 'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/default', 'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/default', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/future', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/default/__tests__', 'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/diviner', 'PhutilDocblockParser' => 'parser/docblock', 'PhutilDocblockParserTestCase' => 'parser/docblock/__tests__', 'PhutilEmailAddress' => 'parser/emailaddress', 'PhutilEmailAddressTestCase' => 'parser/emailaddress/__tests__', 'PhutilErrorHandler' => 'error', 'PhutilEvent' => 'events/event', 'PhutilEventConstants' => 'events/constant/base', 'PhutilEventEngine' => 'events/engine', 'PhutilEventListener' => 'events/listener', 'PhutilEventType' => 'events/constant/type', 'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/excessiveservicecalls', 'PhutilFatalDaemon' => 'daemon/torture/fatal', 'PhutilHangForeverDaemon' => 'daemon/torture/hangforever', 'PhutilInteractiveEditor' => 'console/editor', 'PhutilJSON' => 'parser/json', 'PhutilJSONTestCase' => 'parser/json/__tests__', 'PhutilLanguageGuesser' => 'parser/languageguesser', 'PhutilLanguageGuesserTestCase' => 'parser/languageguesser/__tests__', 'PhutilMarkupEngine' => 'markup/engine', 'PhutilMarkupTestCase' => 'markup/__tests__', 'PhutilMissingSymbolException' => 'symbols/exception/missing', 'PhutilNiceDaemon' => 'daemon/torture/nice', 'PhutilProcessGroupDaemon' => 'daemon/torture/processgroup', 'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/pygments', 'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/rainbow', 'PhutilReadableSerializer' => 'readableserializer', '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', 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/remarkupliteral', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/remarkupnote', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/remarkupquotes', 'PhutilRemarkupEngineTestCase' => 'markup/engine/remarkup/__tests__', 'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/base', 'PhutilRemarkupRuleBold' => 'markup/engine/remarkup/markuprule/bold', 'PhutilRemarkupRuleDel' => 'markup/engine/remarkup/markuprule/del', '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', 'PhutilSaturateStdoutDaemon' => 'daemon/torture/saturatestdout', 'PhutilServiceProfiler' => 'serviceprofiler', 'PhutilSimpleOptions' => 'parser/simpleoptions', 'PhutilSimpleOptionsTestCase' => 'parser/simpleoptions/__tests__', 'PhutilSymbolLoader' => 'symbols', 'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/base', 'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/base', 'PhutilTortureTestDaemon' => 'daemon/torture/base', 'PhutilURI' => 'parser/uri', 'PhutilURITestCase' => 'parser/uri/__tests__', 'PhutilUTF8TestCase' => 'utils/__tests__', 'PhutilUtilsTestCase' => 'utils/__tests__', 'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/xhpast', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/xhpast/__tests__', 'TempFile' => 'filesystem/tempfile', 'TestAbstractDirectedGraph' => 'utils/abstractgraph/__tests__', 'XHPASTNode' => 'parser/xhpast/api/node', '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_mergev' => 'utils', 'array_select_keys' => 'utils', + 'assert_instances_of' => 'utils', 'coalesce' => 'utils', 'csprintf' => 'xsprintf/csprintf', 'exec_manual' => 'future/exec', 'execx' => 'future/exec', 'head' => 'utils', 'hsprintf' => 'markup', 'id' => 'utils', 'idx' => 'utils', 'ifilter' => 'utils', 'igroup' => 'utils', 'ipull' => 'utils', 'isort' => 'utils', 'jsprintf' => 'xsprintf/jsprintf', 'last' => 'utils', 'mfilter' => 'utils', 'mgroup' => 'utils', 'mpull' => 'utils', 'msort' => 'utils', 'newv' => 'utils', 'nonempty' => 'utils', 'phlog' => 'error', 'phutil_console_confirm' => 'console', 'phutil_console_format' => 'console', 'phutil_console_prompt' => 'console', 'phutil_console_require_tty' => 'console', 'phutil_console_wrap' => 'console', 'phutil_deprecated' => 'moduleutils', 'phutil_error_listener_example' => 'error', 'phutil_escape_html' => 'markup', 'phutil_escape_uri' => 'markup', 'phutil_escape_uri_path_component' => 'markup', 'phutil_get_library_name_for_root' => 'moduleutils', 'phutil_get_library_root' => 'moduleutils', 'phutil_get_library_root_for_path' => 'moduleutils', 'phutil_is_utf8' => 'utils', 'phutil_passthru' => 'future/exec', 'phutil_render_tag' => 'markup', 'phutil_unescape_uri_path_component' => 'markup', 'phutil_utf8_hard_wrap_html' => 'utils', 'phutil_utf8_shorten' => 'utils', 'phutil_utf8_strlen' => 'utils', 'phutil_utf8ize' => 'utils', 'phutil_utf8v' => 'utils', '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( 'AbstractDirectedGraphTestCase' => 'ArcanistPhutilTestCase', 'BaseHTTPFuture' => 'Future', 'ConduitFuture' => 'FutureProxy', 'ExecFuture' => 'Future', 'ExecFutureTestCase' => 'ArcanistPhutilTestCase', 'FutureProxy' => 'Future', 'HTTPFuture' => 'BaseHTTPFuture', 'HTTPFutureResponseStatusCURL' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusHTTP' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusParse' => 'HTTPFutureResponseStatus', 'HTTPFutureResponseStatusTransport' => 'HTTPFutureResponseStatus', 'HTTPSFuture' => 'BaseHTTPFuture', 'ImmediateFuture' => 'Future', 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSFuture' => 'FutureProxy', 'PhutilArgumentParserTestCase' => 'ArcanistPhutilTestCase', 'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException', 'PhutilArgumentSpecificationTestCase' => 'ArcanistPhutilTestCase', 'PhutilArgumentUsageException' => 'PhutilArgumentParserException', 'PhutilConsoleWrapTestCase' => 'ArcanistPhutilTestCase', 'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine', 'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy', 'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilDocblockParserTestCase' => 'ArcanistPhutilTestCase', 'PhutilEmailAddressTestCase' => 'ArcanistPhutilTestCase', 'PhutilEventType' => 'PhutilEventConstants', 'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon', 'PhutilFatalDaemon' => 'PhutilTortureTestDaemon', 'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon', 'PhutilJSONTestCase' => 'ArcanistPhutilTestCase', 'PhutilLanguageGuesserTestCase' => 'ArcanistPhutilTestCase', 'PhutilMarkupTestCase' => 'ArcanistPhutilTestCase', 'PhutilNiceDaemon' => 'PhutilTortureTestDaemon', 'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon', 'PhutilRemarkupEngine' => 'PhutilMarkupEngine', 'PhutilRemarkupEngineRemarkupCodeBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupDefaultBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupHeaderBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupInlineBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupListBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupLiteralBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupNoteBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineRemarkupQuotesBlockRule' => 'PhutilRemarkupEngineBlockRule', 'PhutilRemarkupEngineTestCase' => 'ArcanistPhutilTestCase', 'PhutilRemarkupRuleBold' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleDel' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeHTML' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleEscapeRemarkup' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleHyperlink' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleItalic' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleLinebreaks' => 'PhutilRemarkupRule', 'PhutilRemarkupRuleMonospace' => 'PhutilRemarkupRule', 'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon', 'PhutilSimpleOptionsTestCase' => 'ArcanistPhutilTestCase', 'PhutilTortureTestDaemon' => 'PhutilDaemon', 'PhutilURITestCase' => 'ArcanistPhutilTestCase', 'PhutilUTF8TestCase' => 'ArcanistPhutilTestCase', 'PhutilUtilsTestCase' => 'ArcanistPhutilTestCase', 'PhutilXHPASTSyntaxHighlighterTestCase' => 'ArcanistPhutilTestCase', 'TestAbstractDirectedGraph' => 'AbstractDirectedGraph', 'XHPASTNode' => 'AASTNode', 'XHPASTToken' => 'AASTToken', 'XHPASTTree' => 'AASTTree', 'XHPASTTreeTestCase' => 'ArcanistPhutilTestCase', ), 'requires_interface' => array( ), )); diff --git a/src/markup/engine/remarkup/PhutilRemarkupEngine.php b/src/markup/engine/remarkup/PhutilRemarkupEngine.php index 8f3b49e..301c829 100644 --- a/src/markup/engine/remarkup/PhutilRemarkupEngine.php +++ b/src/markup/engine/remarkup/PhutilRemarkupEngine.php @@ -1,165 +1,166 @@ config[$key] = $value; return $this; } public function getConfig($key, $default = null) { return idx($this->config, $key, $default); } public function setBlockRules(array $rules) { + assert_instances_of($rules, 'PhutilRemarkupEngineBlockRule'); $this->blockRules = $rules; return $this; } public function getTextMetadata($key, $default = null) { return idx($this->metadata, $key, $default); } public function setTextMetadata($key, $value) { $this->metadata[$key] = $value; return $this; } public function storeText($text) { return $this->storage->store($text); } public function overwriteStoredText($token, $new_text) { $this->storage->overwrite($token, $new_text); return $this; } public function markupText($text) { return $this->postprocessText($this->preprocessText($text)); } private function setupProcessing() { $this->metadata = array(); $this->storage = new PhutilRemarkupBlockStorage(); $block_rules = $this->blockRules; if (empty($block_rules)) { throw new Exception("Remarkup engine not configured with block rules."); } foreach ($block_rules as $rule) { $rule->setEngine($this); } } public function preprocessText($text) { $this->setupProcessing(); // Apply basic block and paragraph normalization to the text. NOTE: We don't // strip trailing whitespace because it is semantic in some contexts, // notably inlined diffs that the author intends to show as a code block. $text = preg_replace("/\r\n?/", "\n", $text); $text = preg_split("/\n\n/", $text); $block_rules = $this->blockRules; $blocks = array(); $last = null; $last_block = null; foreach ($text as $block) { $action = null; if ($last !== null) { if ($block_rules[$last]->shouldContinueWithBlock($block, $last_block)) { $action = 'merge'; } } if (!$action) { foreach ($block_rules as $key => $block_rule) { if (!$block_rule->shouldMatchBlock(trim($block, "\n"))) { continue; } if (($last !== null) && ($key == $last) && $block_rule->shouldMergeBlocks()) { $action = 'merge'; } else { $action = 'append'; } $last = $key; break; } } $last_block = $block; switch ($action) { case 'merge': end($blocks); $last_block_key = key($blocks); $blocks[$last_block_key]['block'] .= "\n\n".$block; $last_block = $blocks[$last_block_key]['block']; break; case 'append': $blocks[] = array( 'rule' => $block_rules[$last], 'block' => $block, ); break; default: throw new Exception("Block in text did not match any block rule."); } } $output = array(); foreach ($blocks as $block) { $output[] = $block['rule']->markupText($block['block']); } $map = $this->storage->getMap(); unset($this->storage); $metadata = $this->metadata; return array( 'output' => implode("\n\n", $output), 'storage' => $map, 'metadata' => $metadata, ); } public function postprocessText(array $dict) { $this->setupProcessing(); $this->metadata = idx($dict, 'metadata', array()); $this->storage->setMap(idx($dict, 'storage', array())); foreach ($this->blockRules as $block_rule) { $block_rule->postprocess(); } return $this->storage->restore(idx($dict, 'output')); } } diff --git a/src/markup/engine/remarkup/blockrule/base/PhutilRemarkupEngineBlockRule.php b/src/markup/engine/remarkup/blockrule/base/PhutilRemarkupEngineBlockRule.php index 3ca8d89..82081bb 100644 --- a/src/markup/engine/remarkup/blockrule/base/PhutilRemarkupEngineBlockRule.php +++ b/src/markup/engine/remarkup/blockrule/base/PhutilRemarkupEngineBlockRule.php @@ -1,81 +1,82 @@ getBlockPattern(), $block); } public function shouldContinueWithBlock($block, $last_block) { return false; } final public function setEngine(PhutilRemarkupEngine $engine) { $this->engine = $engine; return $this; } final protected function getEngine() { return $this->engine; } public function setMarkupRules(array $rules) { + assert_instances_of($rules, 'PhutilRemarkupRule'); $this->rules = $rules; return $this; } final private function getMarkupRules() { return $this->rules; } final public function postprocess() { $engine = $this->getEngine(); $this->didMarkupText(); foreach ($this->getMarkupRules() as $rule) { $rule->setEngine($engine); $rule->didMarkupText(); } } final protected function applyRules($text) { $engine = $this->getEngine(); foreach ($this->getMarkupRules() as $rule) { $rule->setEngine($engine); $text = $rule->apply($text); } return $text; } } diff --git a/src/markup/engine/remarkup/blockrule/base/__init__.php b/src/markup/engine/remarkup/blockrule/base/__init__.php index c3938a7..e50b09b 100644 --- a/src/markup/engine/remarkup/blockrule/base/__init__.php +++ b/src/markup/engine/remarkup/blockrule/base/__init__.php @@ -1,10 +1,12 @@ ids); } public function current() { return $this->list[$this->key()]; } public function rewind() { $this->pos = 0; } public function valid() { return $this->pos < count($this->ids); } public function next() { $this->pos++; } public function key() { return $this->ids[$this->pos]; } public static function newFromTreeAndNodes(AASTTree $tree, array $nodes) { + assert_instances_of($nodes, 'AASTNode'); $obj = new AASTNodeList(); $obj->tree = $tree; $obj->list = $nodes; $obj->ids = array_keys($nodes); return $obj; } public static function newFromTree(AASTTree $tree) { $obj = new AASTNodeList(); $obj->tree = $tree; $obj->list = array(0 => $tree->getRootNode()); $obj->ids = array(0 => 0); return $obj; } protected function __construct() { } public function getDescription() { if (empty($this->list)) { return 'an empty node list'; } $desc = array(); $desc[] = "a list of ".count($this->list)." nodes:"; foreach ($this->list as $node) { $desc[] = ' '.$node->getDescription().";"; } return implode("\n", $desc); } protected function newList(array $nodes) { return AASTNodeList::newFromTreeAndNodes( $this->tree, $nodes); } public function selectDescendantsOfType($type_name) { $results = array(); foreach ($this->list as $id => $node) { $results += $node->selectDescendantsOfType($type_name)->getRawNodes(); } return $this->newList($results); } public function selectDescendantsOfTypes(array $type_names) { $results = array(); foreach ($type_names as $type_name) { foreach ($this->list as $id => $node) { $results += $node->selectDescendantsOfType($type_name)->getRawNodes(); } } return $this->newList($results); } public function getChildrenByIndex($index) { $results = array(); foreach ($this->list as $id => $node) { $child = $node->getChildByIndex($index); $results[$child->getID()] = $child; } return $this->newList($results); } public function add(AASTNodeList $list) { foreach ($list->list as $id => $node) { $this->list[$id] = $node; } $this->ids = array_keys($this->list); return $this; } protected function executeSelectDescendantsOfType($node, $type) { $results = array(); foreach ($node->getChildren() as $id => $child) { if ($child->getTypeID() == $type) { $results[$id] = $child; } else { $results += $this->executeSelectDescendantsOfType($child, $type); } } return $results; } public function getTokens() { $tokens = array(); foreach ($this->list as $node) { $tokens += $node->getTokens(); } return $tokens; } public function getRawNodes() { return $this->list; } } diff --git a/src/parser/aast/api/list/__init__.php b/src/parser/aast/api/list/__init__.php index 2175e21..4520468 100644 --- a/src/parser/aast/api/list/__init__.php +++ b/src/parser/aast/api/list/__init__.php @@ -1,19 +1,12 @@ doStuff(); * * ...but this works fine: * * id(new Thing())->doStuff(); * * @param wild Anything. * @return wild Unmodified argument. * @group util */ function id($x) { return $x; } /** * Access an array index, retrieving the value stored there if it exists or * a default if it does not. This function allows you to concisely access an * index which may or may not exist without raising a warning. * * @param array Array to access. * @param scalar Index to access in the array. * @param wild Default value to return if the key is not present in the * array. * @return wild If $array[$key] exists, that value is returned. If not, * $default is returned without raising a warning. * @group util */ function idx(array $array, $key, $default = null) { return array_key_exists($key, $array) ? $array[$key] : $default; } /** * Call a method on a list of objects. Short for "method pull", this function * works just like @{function:ipull}, except that it operates on a list of * objects instead of a list of arrays. This function simplifies a common type * of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $key => $object) { * $names[$key] = $object->getName(); * } * * You can express this more concisely with mpull(): * * $names = mpull($objects, 'getName'); * * mpull() takes a third argument, which allows you to do the same but for * the array's keys: * * COUNTEREXAMPLE * $names = array(); * foreach ($objects as $object) { * $names[$object->getID()] = $object->getName(); * } * * This is the mpull version(): * * $names = mpull($objects, 'getName', 'getID'); * * If you pass ##null## as the second argument, the objects will be preserved: * * COUNTEREXAMPLE * $id_map = array(); * foreach ($objects as $object) { * $id_map[$object->getID()] = $object; * } * * With mpull(): * * $id_map = mpull($objects, null, 'getID'); * * See also @{function:ipull}, which works similarly but accesses array indexes * instead of calling methods. * * @param list Some list of objects. * @param string|null Determines which **values** will appear in the result * array. Use a string like 'getName' to store the * value of calling the named method in each value, or * ##null## to preserve the original objects. * @param string|null Determines how **keys** will be assigned in the result * array. Use a string like 'getID' to use the result * of calling the named method as each object's key, or * ##null## to preserve the original keys. * @return dict A dictionary with keys and values derived according * to whatever you passed as $method and $key_method. * @group util */ function mpull(array $list, $method, $key_method = null) { $result = array(); foreach ($list as $key => $object) { if ($key_method !== null) { $key = $object->$key_method(); } if ($method !== null) { $value = $object->$method(); } else { $value = $object; } $result[$key] = $value; } return $result; } /** * Choose an index from a list of arrays. Short for "index pull", this function * works just like @{function:mpull}, except that it operates on a list of * arrays and selects an index from them instead of operating on a list of * objects and calling a method on them. * * This function simplifies a common type of mapping operation: * * COUNTEREXAMPLE * $names = array(); * foreach ($list as $key => $dict) { * $names[$key] = $dict['name']; * } * * With ipull(): * * $names = ipull($list, 'name'); * * See @{function:mpull} for more usage examples. * * @param list Some list of arrays. * @param scalar|null Determines which **values** will appear in the result * array. Use a scalar to select that index from each * array, or null to preserve the arrays unmodified as * values. * @param scalar|null Determines which **keys** will appear in the result * array. Use a scalar to select that index from each * array, or null to preserve the array keys. * @return dict A dictionary with keys and values derived according * to whatever you passed for $index and $key_index. * @group util */ function ipull(array $list, $index, $key_index = null) { $result = array(); foreach ($list as $key => $array) { if ($key_index !== null) { $key = $array[$key_index]; } if ($index !== null) { $value = $array[$index]; } else { $value = $array; } $result[$key] = $value; } return $result; } /** * Group a list of objects by the result of some method, similar to how * GROUP BY works in an SQL query. This function simplifies grouping objects * by some property: * * COUNTEREXAMPLE * $animals_by_species = array(); * foreach ($animals as $animal) { * $animals_by_species[$animal->getSpecies()][] = $animal; * } * * This can be expressed more tersely with mgroup(): * * $animals_by_species = mgroup($animals, 'getSpecies'); * * In either case, the result is a dictionary which maps species (e.g., like * "dog") to lists of animals with that property, so all the dogs are grouped * together and all the cats are grouped together, or whatever super * businessesey thing is actually happening in your problem domain. * * See also @{function:igroup}, which works the same way but operates on * array indexes. * * @param list List of objects to group by some property. * @param string Name of a method, like 'getType', to call on each object * in order to determine which group it should be placed into. * @param ... Zero or more additional method names, to subgroup the * groups. * @return dict Dictionary mapping distinct method returns to lists of * all objects which returned that value. * @group util */ function mgroup(array $list, $by /*, ... */) { $map = mpull($list, $by); $groups = array(); foreach ($map as $group) { // Can't array_fill_keys() here because 'false' gets encoded wrong. $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('mgroup', $args); } } return $groups; } /** * Group a list of arrays by the value of some index. This function is the same * as @{function:mgroup}, except it operates on the values of array indexes * rather than the return values of method calls. * * @param list List of arrays to group by some index value. * @param string Name of an index to select from each array in order to * determine which group it should be placed into. * @param ... Zero or more additional indexes names, to subgroup the * groups. * @return dict Dictionary mapping distinct index values to lists of * all objects which had that value at the index. * @group util */ function igroup(array $list, $by /*, ... */) { $map = ipull($list, $by); $groups = array(); foreach ($map as $group) { $groups[$group] = array(); } foreach ($map as $key => $group) { $groups[$group][$key] = $list[$key]; } $args = func_get_args(); $args = array_slice($args, 2); if ($args) { array_unshift($args, null); foreach ($groups as $group_key => $grouped) { $args[0] = $grouped; $groups[$group_key] = call_user_func_array('igroup', $args); } } return $groups; } /** * Sort a list of objects by the return value of some method. In PHP, this is * often vastly more efficient than ##usort()## and similar. * * // Sort a list of Duck objects by name. * $sorted = msort($ducks, 'getName'); * * It is usually significantly more efficient to define an ordering method * on objects and call ##msort()## than to write a comparator. It is often more * convenient, as well. * * NOTE: This method does not take the list by reference; it returns a new list. * * @param list List of objects to sort by some property. * @param string Name of a method to call on each object; the return values * will be used to sort the list. * @return list Objects ordered by the return values of the method calls. * @group util */ function msort(array $list, $method) { $surrogate = mpull($list, $method); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Sort a list of arrays by the value of some index. This method is identical to * @{function:msort}, but operates on a list of arrays instead of a list of * objects. * * @param list List of arrays to sort by some index value. * @param string Index to access on each object; the return values * will be used to sort the list. * @return list Arrays ordered by the index values. * @group util */ function isort(array $list, $index) { $surrogate = ipull($list, $index); asort($surrogate); $result = array(); foreach ($surrogate as $key => $value) { $result[$key] = $list[$key]; } return $result; } /** * Filter a list of objects by executing a method across all the objects and * filter out the ones wth empty() results. this function works just like * @{function:ifilter}, except that it operates on a list of objects instead * of a list of arrays. * * For example, to remove all objects with no children from a list, where * 'hasChildren' is a method name, do this: * * mfilter($list, 'hasChildren'); * * The optional third parameter allows you to negate the operation and filter * out nonempty objects. To remove all objects that DO have children, do this: * * mfilter($list, 'hasChildren', true); * * @param array List of objects to filter. * @param string A method name. * @param bool Optionally, pass true to drop objects which pass the * filter instead of keeping them. * * @return array List of objects which pass the filter. * @group util */ function mfilter(array $list, $method, $negate = false) { if (!is_string($method)) { throw new InvalidArgumentException('Argument method is not a string.'); } $result = array(); foreach ($list as $key => $object) { $value = $object->$method(); if (!$negate) { if (!empty($value)) { $result[$key] = $object; } } else { if (empty($value)) { $result[$key] = $object; } } } return $result; } /** * Filter a list of arrays by removing the ones with an empty() value for some * index. This function works just like @{function:mfilter}, except that it * operates on a list of arrays instead of a list of objects. * * For example, to remove all arrays without value for key 'username', do this: * * mfilter($list, 'username'); * * The optional third parameter allows you to negate the operation and filter * out nonempty arrays. To remove all arrays that DO have value for key * 'username', do this: * * mfilter($list, 'username', true); * * @param array List of arrays to filter. * @param scalar The index. * @param bool Optionally, pass true to drop arrays which pass the * filter instead of keeping them. * * @return array List of arrays which pass the filter. * @group util */ function ifilter(array $list, $index, $negate = false) { if (!is_scalar($index)) { throw new InvalidArgumentException('Argument index is not a scalar.'); } $result = array(); if (!$negate) { foreach ($list as $key => $array) { if (!empty($array[$index])) { $result[$key] = $array; } } } else { foreach ($list as $key => $array) { if (empty($array[$index])) { $result[$key] = $array; } } } return $result; } /** * Selects a list of keys from an array, returning a new array with only the * key-value pairs identified by the selected keys, in the specified order. * * Note that since this function orders keys in the result according to the * order they appear in the list of keys, there are effectively two common * uses: either reducing a large dictionary to a smaller one, or changing the * key order on an existing dictionary. * * @param dict Dictionary of key-value pairs to select from. * @param list List of keys to select. * @return dict Dictionary of only those key-value pairs where the key was * present in the list of keys to select. Ordering is * determined by the list order. * @group util */ function array_select_keys(array $dict, array $keys) { $result = array(); foreach ($keys as $key) { if (array_key_exists($key, $dict)) { $result[$key] = $dict[$key]; } } return $result; } +/** + * Checks if all values of array are instances of the passed class. + * Throws InvalidArgumentException if it isn't true for any value. + * + * @param array + * @param string + * @return array Returns passed array. + * @group util + */ +function assert_instances_of(array $arr, $class) { + foreach ($arr as $key => $object) { + if (!($object instanceof $class)) { + throw new InvalidArgumentException( + "Array item with key '{$key}' must be an instance of '{$class}'."); + } + } + return $arr; +} + + /** * Returns the first argument which is not strictly null, or ##null## if there * are no such arguments. Identical to the MySQL function of the same name. * * @param ... Zero or more arguments of any type. * @return mixed First non-##null## arg, or null if no such arg exists. * @group util */ function coalesce(/* ... */) { $args = func_get_args(); foreach ($args as $arg) { if ($arg !== null) { return $arg; } } return null; } /** * Similar to @{function:coalesce}, but less strict: returns the first * non-##empty()## argument, instead of the first argument that is strictly * non-##null##. If no argument is nonempty, it returns the last argument. This * is useful idiomatically for setting defaults: * * $display_name = nonempty($user_name, $full_name, "Anonymous"); * * @param ... Zero or more arguments of any type. * @return mixed First non-##empty()## arg, or last arg if no such arg * exists, or null if you passed in zero args. * @group util */ function nonempty(/* ... */) { $args = func_get_args(); $arg = null; foreach ($args as $arg) { if ($arg) { break; } } return $arg; } /** * Invokes the "new" operator with a vector of arguments. There is no way to * call_user_func_array() on a class constructor, so you can instead use this * function: * * $obj = newv($class_name, $argv); * * That is, these two statements are equivalent: * * $pancake = new Pancake('Blueberry', 'Maple Syrup', true); * $pancake = newv('Pancake', array('Blueberry', 'Maple Syrup', true)); * * DO NOT solve this problem in other, more creative ways! Three popular * alternatives are: * * - Build a fake serialized object and unserialize it. * - Invoke the constructor twice. * - just use eval() lol * * These are really bad solutions to the problem because they can have side * effects (e.g., __wakeup()) and give you an object in an otherwise impossible * state. Please endeavor to keep your objects in possible states. * * If you own the classes you're doing this for, you should consider whether * or not restructuring your code (for instance, by creating static * construction methods) might make it cleaner before using newv(). Static * constructors can be invoked with call_user_func_array(), and may give your * class a cleaner and more descriptive API. * * @param string The name of a class. * @param list Array of arguments to pass to its constructor. * @return obj A new object of the specified class, constructed by passing * the argument vector to its constructor. * @group util */ function newv($class_name, array $argv) { $reflector = new ReflectionClass($class_name); if ($argv) { return $reflector->newInstanceArgs($argv); } else { return $reflector->newInstance(); } } /** * Returns the first element of an array. Exactly like reset(), but doesn't * choke if you pass it some non-referenceable value like the return value of * a function. * * @param array Array to retrieve the first element from. * @return wild The first value of the array. * @group util */ function head(array $arr) { return reset($arr); } /** * Returns the last element of an array. This is exactly like end() except * that it won't warn you if you pass some non-referencable array to * it -- e.g., the result of some other array operation. * * @param array Array to retrieve the last element from. * @return wild The last value of the array. * @group util */ function last($arr) { return end($arr); } /** * Merge a vector of arrays performantly. This has the same semantics as * array_merge(), so these calls are equivalent: * * array_merge($a, $b, $c); * array_mergev(array($a, $b, $c)); * * However, when you have a vector of arrays, it is vastly more performant to * merge them with this function than by calling array_merge() in a loop, * because using a loop generates an intermediary array on each iteration. * * @param list Vector of arrays to merge. * @return list Arrays, merged with array_merge() semantics. * @group util */ function array_mergev(array $arrayv) { if (!$arrayv) { return array(); } return call_user_func_array('array_merge', $arrayv); }