diff --git a/composer.json b/composer.json index 397fdf0..e358ab2 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "symfony-cmf/routing": "1.1.*@alpha", "easyrdf/easyrdf": "0.8.*@beta", "phpunit/phpunit": "3.7.*", - "zendframework/zend-feed": "2.2.*" + "zendframework/zend-feed": "2.2.*", + "sdboyer/gliph": "0.1.*" }, "autoload": { "psr-0": { diff --git a/composer.lock b/composer.lock index decd030..3080532 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "204b3755db988998bcc618e71b2f235c", + "hash": "38591fb50ec8bc7c3f2d2a9ea542246f", "packages": [ { "name": "doctrine/annotations", @@ -1112,6 +1112,50 @@ "time": "2012-12-21 11:40:51" }, { + "name": "sdboyer/gliph", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/sdboyer/gliph.git", + "reference": "c7bd13eb2e6b51b017f025e24a7a676a4eed1a4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sdboyer/gliph/zipball/c7bd13eb2e6b51b017f025e24a7a676a4eed1a4a", + "reference": "c7bd13eb2e6b51b017f025e24a7a676a4eed1a4a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "type": "library", + "autoload": { + "psr-0": { + "Gliph": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Boyer", + "email": "tech@samboyer.org" + } + ], + "description": "A graph library for PHP.", + "homepage": "http://github.com/sdboyer/gliph", + "keywords": [ + "gliph", + "graph", + "library", + "php", + "spl" + ], + "time": "2013-09-14 04:16:01" + }, + { "name": "symfony-cmf/routing", "version": "1.1.0-beta1", "target-dir": "Symfony/Cmf/Component/Routing", diff --git a/core/core.services.yml b/core/core.services.yml index 7d56112..8b2182b 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -641,3 +641,6 @@ services: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + asset.library_repository: + class: Drupal\Core\Asset\AssetLibraryRepository + arguments: ['@module_handler'] diff --git a/core/includes/common.inc b/core/includes/common.inc index be97724..e113493 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1629,6 +1629,9 @@ function drupal_add_css($data = NULL, $options = NULL) { // Create an array of CSS files for each media type first, since each type needs to be served // to the browser differently. if (isset($data)) { + $options['type'] = isset($options['type']) ? $options['type'] : 'file'; + drupal_collect_assets($data, $options, 'css'); + $options += array( 'type' => 'file', 'group' => CSS_AGGREGATE_DEFAULT, @@ -1680,6 +1683,29 @@ function drupal_add_css($data = NULL, $options = NULL) { return $css; } +function drupal_collect_assets($data, $options, $type = '') { + $bag = &drupal_static('global_asset_bag', FALSE); + $collector = &drupal_static('global_asset_collector', FALSE); + + $bag = ($bag instanceof \Drupal\Core\Asset\Bag\AssetBag) ? $bag : new \Drupal\Core\Asset\Bag\AssetBag(); + if (!$collector instanceof \Drupal\Core\Asset\Factory\AssetCollector) { + $collector = new \Drupal\Core\Asset\Factory\AssetCollector(); + $collector->setBag($bag); + } + + if ($data instanceof \Drupal\Core\Asset\AssetInterface) { + $collector->add($data); + return; + } + + if ($type == 'js-setting') { + // TODO handle js settings + return; + } + + $collector->create($type, $options['type'], $data, $options); +} + /** * Returns a themed representation of all stylesheets to attach to the page. * @@ -2199,6 +2225,11 @@ function drupal_html_id($id) { function drupal_add_js($data = NULL, $options = NULL) { $javascript = &drupal_static(__FUNCTION__, array()); + if (isset($data)) { + $options['type'] = isset($options['type']) ? $options['type'] : 'file'; + drupal_collect_assets($data, $options, $options['type'] == 'setting' ? 'js-setting' : 'js'); + } + // Construct the options, taking the defaults into consideration. if (isset($options)) { if (!is_array($options)) { diff --git a/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php new file mode 100644 index 0000000..8d37137 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/AssetAggregateInterface.php @@ -0,0 +1,53 @@ +metadata = $metadata; + $this->sourceRoot = $sourceRoot; + $this->assetStorage = new \SplObjectStorage(); + $this->nestedStorage = new \SplObjectStorage(); + + $this->filters = new FilterCollection($filters); + + foreach ($assets as $asset) { + $this->add($asset); + } + } + + /** + * {@inheritdoc} + */ + public function id() { + if (empty($this->id)) { + $this->calculateId(); + } + + return $this->id; + } + + /** + * Calculates and stores an id for this aggregate from the contained assets. + * + * @return void + */ + protected function calculateId() { + $id = ''; + foreach ($this->assetStorage as $asset) { + // Preserve a little id stability by not composing id from aggregates + if (!$asset instanceof AssetAggregateInterface) { + $id .= $asset->id(); + } + } + // TODO come up with something stabler/more serialization friendly than object hash + $this->id = hash('sha256', $id ?: spl_object_hash($this)); + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + // TODO should this immutable? doable if we further granulate the interfaces + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function add(AsseticAssetInterface $asset) { + if (!$asset instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($asset); + + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + + if ($asset instanceof AssetAggregateInterface) { + $this->nestedStorage->attach($asset); + } + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + if ($this->assetStorage->contains($asset)) { + return TRUE; + } + + foreach ($this->nestedStorage as $aggregate) { + if ($aggregate->contains($asset)) { + return TRUE; + } + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function reindex() { + $map = array(); + foreach ($this->assetIdMap as $asset) { + $map[$asset->id()] = $asset; + } + $this->assetIdMap = $map; + + // Recalculate the id, too. + $this->calculateId(); + + // Recursively reindex contained aggregates. + foreach ($this->nestedStorage as $aggregate) { + $aggregate->reindex(); + } + } + + /** + * {@inheritdoc} + */ + public function getById($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else { + // Recursively search for the id + foreach ($this->nestedStorage as $aggregate) { + if ($found = $aggregate->getById($id)) { + return $found; + } + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This aggregate does not contain an asset with id %s.', $id)); + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = TRUE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + + return $this->removeLeaf($needle, $graceful); + } + + /** + * {@inheritdoc} + */ + public function removeLeaf(AsseticAssetInterface $needle, $graceful = FALSE) { + if (!$needle instanceof AssetInterface) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($needle); + + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->assetIdMap[$id], $this->nestedStorage[$asset]); + + return TRUE; + } + + if ($asset instanceof AssetAggregateInterface && $asset->removeLeaf($needle, $graceful)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Asset not found.'); + } + + /** + * {@inheritdoc} + */ + public function replace($needle, AssetInterface $replacement, $graceful = TRUE) { + if (is_string($needle)) { + if (!$needle = $this->getById($needle, $graceful)) { + return FALSE; + } + } + + return $this->replaceLeaf($needle, $replacement, $graceful); + } + + /** + * {@inheritdoc} + */ + public function replaceLeaf(AsseticAssetInterface $needle, AsseticAssetInterface $replacement, $graceful = FALSE) { + if (!($needle instanceof AssetInterface && $replacement instanceof AssetInterface)) { + throw new UnsupportedAsseticBehaviorException('Vanilla Assetic asset(s) provided; Drupal aggregates require Drupal-flavored assets.'); + } + $this->ensureCorrectType($needle); + $this->ensureCorrectType($replacement); + + foreach ($this->assetIdMap as $id => $asset) { + if ($asset === $needle) { + unset($this->assetStorage[$asset], $this->nestedStorage[$asset]); + + array_splice($this->assetIdMap, $i, 1, array($replacement->id() => $replacement)); + $this->assetStorage->attach($replacement); + if ($replacement instanceof AssetAggregateInterface) { + $this->nestedStorage->attach($replacement); + } + + return TRUE; + } + + if ($asset instanceof AssetAggregateInterface && $asset->replaceLeaf($needle, $replacement, $graceful)) { + return TRUE; + } + } + + if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException('Asset not found.'); + } + + /** + * {@inheritdoc} + * + * Aggregate assets are inherently eligible for preprocessing, so this is + * always true. + */ + public function isPreprocessable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + */ + public function ensureFilter(FilterInterface $filter) { + $this->filters->ensure($filter); + } + + /** + * {@inheritdoc} + */ + public function getFilters() { + return $this->filters->all(); + } + + /** + * {@inheritdoc} + */ + public function clearFilters() { + $this->filters->clear(); + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // loop through leaves and load each asset + $parts = array(); + foreach ($this as $asset) { + $asset->load($additionalFilter); + $parts[] = $asset->getContent(); + } + + $this->content = implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + // loop through leaves and dump each asset + $parts = array(); + foreach ($this as $asset) { + $parts[] = $asset->dump($additionalFilter); + } + + return implode("\n", $parts); + } + + /** + * {@inheritdoc} + */ + public function getContent() { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) { + $this->content = $content; + } + + /** + * {@inheritdoc} + */ + public function getSourceRoot() { + return $this->sourceRoot; + } + + /** + * {@inheritdoc} + */ + public function getSourcePath() { + } + + /** + * {@inheritdoc} + */ + public function getTargetPath() { + return $this->targetPath; + } + + /** + * {@inheritdoc} + */ + public function setTargetPath($targetPath) { + $this->targetPath = $targetPath; + } + + /** + * Returns the highest last-modified value of all contained assets. + * + * @return integer|null + * A UNIX timestamp + */ + public function getLastModified() { + if (!count($this->assetStorage)) { + return; + } + + $mtime = 0; + foreach ($this->assetStorage as $asset) { + $assetMtime = $asset->getLastModified(); + if ($assetMtime > $mtime) { + $mtime = $assetMtime; + } + } + + return $mtime; + } + + /** + * TODO Assetic uses their iterator to clone, then populate values and return here; is that a good model for us? + */ + public function getIterator() { + // TODO this is totally junk + return new \ArrayIterator($this->assetIdMap); + } + + /** + * Indicates whether this collection contains any assets. + * + * @return bool + * TRUE if contained assets are present, FALSE otherwise. + */ + public function isEmpty() { + return $this->assetStorage->count() === 0; + } + + + /** + * Ensures that the asset is of the correct subtype (e.g., css vs. js). + * + * @param AssetInterface $asset + * + * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + abstract protected function ensureCorrectType(AssetInterface $asset); +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php new file mode 100644 index 0000000..c400772 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Aggregate/CssAggregateAsset.php @@ -0,0 +1,41 @@ +hasVertex($vertex)) { + $this->vertices[$vertex] = new \SplObjectStorage(); + $this->verticesById[$vertex->id()] = $vertex; + $this->processNewVertex($vertex); + } + } + + /** + * Processes all sequencing information for a given vertex. + * + * @param AssetInterface $vertex + */ + protected function processNewVertex(AssetInterface $vertex) { + $id = $vertex->id(); + // First, check if anything has a watch out for this vertex. + if (isset($this->before[$id])) { + foreach ($this->before[$id] as $predecessor) { + $this->addDirectedEdge($predecessor, $vertex); + } + unset($this->before[$id]); + } + + if (isset($this->after[$id])) { + foreach ($this->after[$id] as $successor) { + $this->addDirectedEdge($vertex, $successor); + } + unset($this->after[$id]); + } + + // Add watches for this vertex, if it implements the interface. + if ($vertex instanceof AssetOrderingInterface) { + // TODO this logic assumes collections enforce uniqueness - ensure that's the case. + foreach ($vertex->getPredecessors() as $predecessor) { + // Normalize to id string. + $predecessor = is_string($predecessor) ? $predecessor : $predecessor->id(); + + if (isset($this->verticesById[$predecessor])) { + $this->addDirectedEdge($this->verticesById[$predecessor], $vertex); + } + else { + if (!isset($this->before[$predecessor])) { + $this->before[$predecessor] = array(); + } + $this->before[$predecessor][] = $vertex; + } + } + + foreach ($vertex->getSuccessors() as $successor) { + // Normalize to id string. + $successor = is_string($successor) ? $successor : $successor->id(); + + if (isset($this->verticesById[$successor])) { + $this->addDirectedEdge($vertex, $this->verticesById[$successor]); + } + else { + if (!isset($this->before[$successor])) { + $this->after[$successor] = array(); + } + $this->after[$successor][] = $vertex; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function transpose() { + // TODO super-important - have to rewrite transpose so that it correctly inverts edge direction + return parent::transpose(); + } +} diff --git a/core/lib/Drupal/Core/Asset/AssetInterface.php b/core/lib/Drupal/Core/Asset/AssetInterface.php new file mode 100644 index 0000000..bbfb1ee --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetInterface.php @@ -0,0 +1,52 @@ +moduleHandler = $module_handler; + } + + protected function initialize() { + if ($this->initialized) { + return; + } + $this->initialized = TRUE; + + $library_collector = new AssetLibraryCollector($this); + foreach ($this->moduleHandler->getImplementations('library_info') as $module) { + $library_collector->setModule($module); + $libraries = call_user_func("{$module}_library_info"); + foreach ($libraries as $name => $info) { + // Normalize - apparently hook_library_info is allowed to be sloppy. + $info += array('dependencies' => array(), 'js' => array(), 'css' => array()); + + // @todo This works sorta sanely because of the array_intersect_key() hack in AssetLibrary::construct() + $asset_collector = $library_collector->buildLibrary($name, $info); + foreach (array('js', 'css') as $type) { + if (!empty($info[$type])) { + foreach ($info[$type] as $data => $options) { + if (is_scalar($options)) { + $data = $options; + $options = array(); + } + // @todo good enough for now to assume these are all file assets + $asset_collector->create($type, 'file', $data, $options); + } + } + } + } + } + } + + /** + * Gets a library by composite key. + * + * @param string $module + * The module owner that declared the library. + * + * @param string $name + * The library name. + * + * @return \Drupal\Core\Asset\Bag\AssetLibrary + * The requested library. + * + * @throws \InvalidArgumentException If there is no library by that name + */ + public function get($module, $name) { + $this->initialize(); + if (!isset($this->libraries[$module][$name])) { + throw new \InvalidArgumentException(sprintf('There is no library identified by "%s/%s" in the manager.', $module, $name)); + } + + return $this->libraries[$module][$name]; + } + + /** + * Checks if the current library manager has a certain library. + * + * @param string $module + * The module owner that declared the library. + * + * @param string $name + * The library name. + * + * @return bool + * True if the library has been set, false if not + */ + public function has($module, $name) { + $this->initialize(); + return isset($this->libraries[$module][$name]); + } + + public function add($module, $name, AssetLibrary $library) { + // TODO add validation - alphanum + underscore only + if (!isset($this->libraries[$module])) { + $this->libraries[$module] = array(); + } + + $this->libraries[$module][$name] = $library; + $this->flattened = NULL; + } + + /** + * Retrieves the asset objects on which the passed asset depends. + * + * @param AssetOrderingInterface $asset + * The asset whose dependencies should be retrieved. + * + * @return array + * An array of AssetInterface objects if any dependencies were found; + * otherwise, an empty array. + */ + public function resolveDependencies(AssetOrderingInterface $asset) { + $dependencies = array(); + + if ($asset->hasDependencies()) { + foreach ($asset->getDependencyInfo() as $info) { + try { + $dependencies[] = $this->get($info[0], $info[1]); + } + // TODO should we really try/catch at a potentially high traffic place like this? + catch (\InvalidArgumentException $e) { + // TODO we're relying on a method that's not in AssetOrderingInterface... + watchdog('assets', 'Asset @asset declared a dependency on nonexistent library @module/@name', array($asset->getSourcePath(), $info[0], $info[1]), WATCHDOG_ERROR); + } + } + } + + return $dependencies; + } + + /** + * Returns an array of library names. + * + * @return array An array of library names + */ + public function getNames() { + $this->initialize(); + return array_keys($this->libraries); + } + + /** + * Clears all libraries. + */ + public function clear() { + $this->initialize(); + $this->libraries = array(); + $this->flattened = NULL; + } + + public function getIterator() { + $this->initialize(); + if (is_null($this->flattened)) { + $this->flattened = array(); + foreach ($this->libraries as $module => $set) { + foreach ($set as $name => $library) { + $this->flattened["$module:$name"] = $library; + } + } + } + + return new \ArrayIterator($this->flattened); + } + +} diff --git a/core/lib/Drupal/Core/Asset/AssetOrderingInterface.php b/core/lib/Drupal/Core/Asset/AssetOrderingInterface.php new file mode 100644 index 0000000..7fe5835 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/AssetOrderingInterface.php @@ -0,0 +1,111 @@ +js = new JsCollection(); + $this->css = new CssCollection(); + } + + /** + * {@inheritdoc} + */ + public function add(AssetInterface $asset) { + if ($this->isFrozen()) { + throw new \LogicException('Assets cannot be added to a frozen AssetBag.', E_ERROR); + } + + if ($asset instanceof JavascriptAssetInterface) { + $this->js->add($asset); + } + if ($asset instanceof StylesheetAssetInterface) { + $this->css->add($asset); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addAssetBag(AssetBagInterface $bag, $freeze = TRUE) { + if ($this->isFrozen()) { + throw new \LogicException('Assets cannot be added to a frozen AssetBag.', E_ERROR); + } + + if ($bag->hasCss()) { + $this->css->mergeCollection($bag->getCss()); + } + if ($bag->hasJs()) { + $this->js->mergeCollection($bag->getJs()); + } + + if ($freeze) { + $bag->freeze(); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasCss() { + return !$this->css->isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function getCss() { + return $this->css; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assets; + } + + /** + * {@inheritdoc} + * + * TODO js settings need a complete overhaul + */ + public function addJsSetting($data) { + $this->javascript['settings']['data'][] = $data; + } + + /** + * {@inheritdoc} + */ + public function hasJs() { + return !$this->js->isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function getJs() { + return $this->js; + } + + /** + * {@inheritdoc} + */ + public function freeze() { + $this->frozen = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() { + return $this->frozen; + } + + /** + * {@inheritdoc} + */ + public function resolveDependencies(AssetLibraryRepository $repository) { + foreach ($this->css as $asset) { + foreach ($repository->resolveDependencies($asset) as $dep) { + $this->add($dep); + } + } + + foreach ($this->js as $asset) { + foreach ($repository->resolveDependencies($asset) as $dep) { + $this->add($dep); + } + } + } + +} diff --git a/core/lib/Drupal/Core/Asset/Bag/AssetBagInterface.php b/core/lib/Drupal/Core/Asset/Bag/AssetBagInterface.php new file mode 100644 index 0000000..21223ec --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Bag/AssetBagInterface.php @@ -0,0 +1,114 @@ + $val) { + $this->$key = $val; + } + } + + /** + * Set the asset library's title. + * + * @param string $title + * The title of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setTitle($title) { + $this->attemptWrite(); + $this->title = $title; + return $this; + } + + /** + * Get the asset library's title. + * + * @return string + * The title of the asset library. + */ + public function getTitle() { + return $this->title; + } + + /** + * Set the asset library's website. + * + * @param string $website + * The website of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setWebsite($website) { + $this->attemptWrite(); + $this->website = $website; + return $this; + } + + /** + * Get the asset library's website. + * + * @return string + * The website of the asset library. + */ + public function getWebsite() { + return $this->website; + } + + /** + * Set the asset library's version. + * + * @param string $version + * The version of the asset library. + * + * @return \Drupal\Core\Asset\AssetLibrary + * The asset library, to allow for chaining. + */ + public function setVersion($version) { + $this->attemptWrite(); + $this->version = $version; + return $this; + } + + /** + * Get the asset library's version. + * + * @return string + * The version of the asset library. + */ + public function getVersion() { + return $this->version; + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + $this->attemptWrite(); + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->attemptWrite(); + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencyInfo() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function before($asset) { + $this->successors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function after($asset) { + $this->predecessors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + } + + /** + * Checks if the asset library is frozen, throws an exception if it is. + */ + protected function attemptWrite() { + if ($this->isFrozen()) { + throw new \LogicException('Metadata cannot be modified on a frozen AssetLibrary.'); + } + } +} diff --git a/core/lib/Drupal/Core/Asset/BaseAsset.php b/core/lib/Drupal/Core/Asset/BaseAsset.php new file mode 100644 index 0000000..48a3038 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseAsset.php @@ -0,0 +1,253 @@ +filters = new FilterCollection($filters); + $this->sourceRoot = $sourceRoot; + $this->sourcePath = $sourcePath; + $this->loaded = FALSE; + $this->metadata = $metadata; + } + + public function __clone() { + $this->filters = clone $this->filters; + $this->metadata = clone $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function getMetadata() { + return $this->metadata; + } + + /** + * {@inheritdoc} + */ + public function ensureFilter(FilterInterface $filter) { + $this->filters->ensure($filter); + } + + /** + * {@inheritdoc} + */ + public function getFilters() { + return $this->filters->all(); + } + + /** + * {@inheritdoc} + */ + public function clearFilters() { + $this->filters->clear(); + } + + /** + * Encapsulates asset loading logic. + * + * @param string $content The asset content + * @param FilterInterface $additionalFilter An additional filter + */ + protected function doLoad($content, FilterInterface $additionalFilter = NULL) { + $filter = clone $this->filters; + if ($additionalFilter) { + $filter->ensure($additionalFilter); + } + + $asset = clone $this; + $asset->setContent($content); + + $filter->filterLoad($asset); + $this->content = $asset->getContent(); + + $this->loaded = TRUE; + } + + /** + * {@inheritdoc} + */ + public function dump(FilterInterface $additionalFilter = NULL) { + if (!$this->loaded) { + $this->load(); + } + + $filter = clone $this->filters; + if ($additionalFilter) { + $filter->ensure($additionalFilter); + } + + $asset = clone $this; + $filter->filterDump($asset); + + return $asset->getContent(); + } + + /** + * {@inheritdoc} + */ + public function getContent() { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) { + $this->content = $content; + } + + /** + * {@inheritdoc} + */ + public function getSourceRoot() { + return $this->sourceRoot; + } + + /** + * {@inheritdoc} + */ + public function getSourcePath() { + return $this->sourcePath; + } + + /** + * {@inheritdoc} + */ + public function getTargetPath() { + return $this->targetPath; + } + + /** + * {@inheritdoc} + */ + public function setTargetPath($targetPath) { + $this->targetPath = $targetPath; + } + + /** + * {@inheritdoc} + */ + public function isPreprocessable() { + return (bool) $this->metadata->get('preprocess'); + } + + public function setDefaults(array $defaults) { + $this->metadataDefaults = $defaults; + } + + /** + * {@inheritdoc} + */ + public function hasDependencies() { + return !empty($this->dependencies); + } + + /** + * {@inheritdoc} + */ + public function addDependency($module, $name) { + $this->dependencies[] = array($module, $name); + } + + /** + * {@inheritdoc} + */ + public function clearDependencies() { + $this->dependencies = array(); + } + + /** + * {@inheritdoc} + */ + public function getDependencyInfo() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function before($asset) { + $this->successors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function after($asset) { + $this->predecessors[] = $asset; + } + + /** + * {@inheritdoc} + */ + public function getPredecessors() { + return $this->predecessors; + } + + /** + * {@inheritdoc} + */ + public function getSuccessors() { + return $this->successors; + } + + /** + * {@inheritdoc} + */ + public function clearSuccessors() { + $this->successors = array(); + } + + /** + * {@inheritdoc} + */ + public function clearPredecessors() { + $this->predecessors = array(); + } +} diff --git a/core/lib/Drupal/Core/Asset/BaseExternalAsset.php b/core/lib/Drupal/Core/Asset/BaseExternalAsset.php new file mode 100644 index 0000000..183bd2e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseExternalAsset.php @@ -0,0 +1,71 @@ +sourceUrl = $sourceUrl; + $this->ignoreErrors = FALSE; // TODO expose somehow + + list($scheme, $url) = explode('://', $sourceUrl, 2); + list($host, $path) = explode('/', $url, 2); + + parent::__construct($metadata, $filters, $scheme.'://'.$host, $path); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->sourceUrl; + } + + /** + * Returns the time the current asset was last modified. + * + * @todo copied right from Assetic. needs to be made more Drupalish. + * + * @return integer|null A UNIX timestamp + */ + public function getLastModified() { + if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(array('http' => array('method' => 'HEAD'))))) { + foreach ($http_response_header as $header) { + if (0 === stripos($header, 'Last-Modified: ')) { + list(, $mtime) = explode(':', $header, 2); + + return strtotime(trim($mtime)); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function load(FilterInterface $additionalFilter = NULL) { + // TODO dumb and kinda wrong, decide how to do this right. + throw new UnsupportedAsseticBehaviorException('Drupal does not support the retrieval or manipulation of remote assets.'); + } + +} diff --git a/core/lib/Drupal/Core/Asset/BaseFileAsset.php b/core/lib/Drupal/Core/Asset/BaseFileAsset.php new file mode 100644 index 0000000..49e7352 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseFileAsset.php @@ -0,0 +1,64 @@ +source = $source; + + parent::__construct($metadata, $filters, $sourceRoot, $sourcePath); + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->source; + } + + /** + * Returns the time the current asset was last modified. + * + * @return integer|null A UNIX timestamp + */ + public function getLastModified() { + if (!is_file($this->source)) { + throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source)); + } + + return filemtime($this->source); + } + + /** + * Loads the asset into memory and applies load filters. + * + * You may provide an additional filter to apply during load. + * + * @todo copied right from Assetic. needs to be made more Drupalish. + * + * @param FilterInterface $additionalFilter An additional filter + */ + public function load(FilterInterface $additionalFilter = NULL) { + if (!is_file($this->source)) { + throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $this->source)); + } + + $this->doLoad(file_get_contents($this->source), $additionalFilter); + } + +} diff --git a/core/lib/Drupal/Core/Asset/BaseStringAsset.php b/core/lib/Drupal/Core/Asset/BaseStringAsset.php new file mode 100644 index 0000000..ef5ddeb --- /dev/null +++ b/core/lib/Drupal/Core/Asset/BaseStringAsset.php @@ -0,0 +1,44 @@ +content = $content; + $this->lastModified = REQUEST_TIME; // TODO this is terrible + + parent::__construct($metadata, $filters); + } + + /** + * {@inheritdoc} + */ + public function id() { + // TODO hashing current content means this id is essentially useless. + return md5($this->content); + } + + public function setLastModified($last_modified) { + $this->lastModified = $last_modified; + } + + public function getLastModified() { + return $this->lastModified; + } + + public function load(FilterInterface $additionalFilter = NULL) { + $this->doLoad($this->content, $additionalFilter); + } +} diff --git a/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php new file mode 100644 index 0000000..f63853c --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/AssetCollectionBasicInterface.php @@ -0,0 +1,88 @@ +assetStorage = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function add(AssetInterface $asset) { + $this->attemptWrite(); + $this->ensureCorrectType($asset); + + $this->assetStorage->attach($asset); + $this->assetIdMap[$asset->id()] = $asset; + } + + /** + * {@inheritdoc} + */ + public function contains(AssetInterface $asset) { + return $this->assetStorage->contains($asset); + } + + /** + * {@inheritdoc} + */ + public function getById($id, $graceful = TRUE) { + if (isset($this->assetIdMap[$id])) { + return $this->assetIdMap[$id]; + } + else if ($graceful) { + return FALSE; + } + + throw new \OutOfBoundsException(sprintf('This collection does not contain an asset with id %s.', $id)); + } + /** + * {@inheritdoc} + */ + public function reindex() { + $map = array(); + foreach ($this->assetIdMap as $asset) { + $map[$asset->id()] = $asset; + } + $this->assetIdMap = $map; + } + + /** + * {@inheritdoc} + */ + public function remove($needle, $graceful = TRUE) { + $this->attemptWrite(); + + if ((is_string($needle) && $needle = $this->getById($needle, $graceful)) || + $needle instanceof AssetInterface) { + unset($this->assetIdMap[$needle->id()], $this->assetStorage[$needle]); + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function all() { + return $this->assetIdMap; + } + + /** + * {@inheritdoc} + */ + public function mergeCollection(AssetCollectionInterface $collection) { + $this->attemptWrite(); + // TODO subtype mismatch checking + + $other_assets = $collection->all(); + + foreach (array_intersect_key($this->assetIdMap, $other_assets) as $id => $asset) { + unset($other_assets[$id]); + } + + foreach ($other_assets as $asset) { + $this->add($asset); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function freeze() { + $this->frozen = TRUE; + } + + /** + * {@inheritdoc} + */ + public function isFrozen() { + return $this->frozen; + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + return new \ArrayIterator($this->assetIdMap); + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + return empty($this->assetIdMap); + } + + /** + * Checks if the asset library is frozen, throws an exception if it is. + */ + protected function attemptWrite() { + if ($this->isFrozen()) { + throw new \LogicException('Cannot write to a frozen AssetCollection.'); + } + } + + /** + * Ensures that the asset is of the correct subtype (e.g., css vs. js). + * + * @param AssetInterface $asset + * + * @throws \Drupal\Core\Asset\Exception\AssetTypeMismatchException + */ + abstract protected function ensureCorrectType(AssetInterface $asset); +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Collection/CssCollection.php b/core/lib/Drupal/Core/Asset/Collection/CssCollection.php new file mode 100644 index 0000000..ef650fe --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Collection/CssCollection.php @@ -0,0 +1,26 @@ +repository = $repository; + } + + /** + * Groups a collection of assets into logical groups of asset collections. + * + * @param array $assets + * An asset collection. + * TODO update the interface to be an AssetCollection, not an array + * + * @return array + * A sorted array of asset groups. + */ + public function group(CssCollection $assets) { + $tsl = $this->getOptimalTSL($assets); + + // TODO replace with CssCollection + // TODO ordering suddenly matters here...problem? + $processed = new CssCollection(); + $last_key = FALSE; + foreach ($tsl as $asset) { + // TODO fix the visitor - this will fail right now because the optimality data got depleted during traversal + $key = $this->optimal_lookup->contains($asset) ? $this->optimal_lookup[$asset] : FALSE; + + if ($key !== $last_key) { + $processed[] = $aggregate = new CssAggregateAsset($asset->getMetadata()); + } + + $aggregate->add($asset); + } + + return $processed; + } + + /** + * Gets a topologically sorted list that is optimal for grouping. + * + * @param array $assets + * + * @return array + * A linear list of assets that will enable optimal groupings. + * + * @throws \LogicException + */ + protected function getOptimalTSL(CssCollection $assets) { + // We need to define the optimum minimal group set, given metadata + // boundaries across which aggregates cannot be safely made. + $this->optimal = array(); + + // Also create an SplObjectStorage to act as a lookup table on an asset to + // its group, if any. + $this->optimal_lookup = new \SplObjectStorage(); + + // Finally, create a specialized directed adjacency list that will capture + // sequencing information. + $graph = new AssetGraph(); + + foreach ($assets as $asset) { + $graph->addVertex($asset); + + $k = $this->getGroupKey($asset); + + if ($k === FALSE) { + // Record no optimality information for ungroupable assets; they will + // be visited normally and rearranged as needed. + continue; + } + + if (!isset($this->optimal[$k])) { + // Create an SplObjectStorage to represent each set of assets that would + // optimally be grouped together. + $this->optimal[$k] = new \SplObjectStorage(); + } + $this->optimal[$k]->attach($asset, $k); + $this->optimal_lookup->attach($asset, $this->optimal[$k]); + } + + // First, transpose the graph in order to get an appropriate answer + $transpose = $graph->transpose(); + + // Create a queue of start vertices to prime the traversal. + $queue = $this->createSourceQueue($graph, $transpose); + + // Now, create the visitor and walk the graph to get an optimal TSL. + $visitor = new OptimallyGroupedTSLVisitor($this->optimal, $this->optimal_lookup); + DepthFirst::traverse($transpose, $visitor, $queue); + + return $visitor->getTSL(); + } + + /** + * Gets the grouping key for the provided asset. + * + * @param $asset + * + * @return bool|string + * @throws \UnexpectedValueException + */ + protected function getGroupKey(StylesheetAssetInterface $asset) { + $meta = $asset->getMetadata(); + // The browsers for which the CSS item needs to be loaded is part of the + // information that determines when a new group is needed, but the order + // of keys in the array doesn't matter, and we don't want a new group if + // all that's different is that order. + $browsers = $meta->get('browsers'); + ksort($browsers); + + if ($asset instanceof StylesheetFileAsset) { + // Compose a string key out of the set of relevant properties. + // TODO - currently ignoring group, which is used in the current implementation. wishful thinking? maybe, maybe not. + // TODO media has been pulled out - needs to be handled by the aggregator, wrapping css in media queries + $k = $asset->isPreprocessable() + ? implode(':', array('file', $meta->get('every_page'), implode('', $browsers))) + : FALSE; + + return $k; + } + else if ($asset instanceof StylesheetStringAsset) { + // String items are always grouped. + // TODO use the term 'inline' here? do "string" and "inline" necessarily mean the same? + $k = implode(':', 'string', implode('', $browsers)); + + return $k; + } + else if ($asset instanceof StylesheetExternalAsset) { + // Never group external assets. + $k = FALSE; + + return $k; + } + else { + throw new \UnexpectedValueException(sprintf('Unknown CSS asset type "%s" somehow made it into the CSS collection during grouping.', get_class($asset))); + } + } + + /** + * Creates a queue of starting vertices that will facilitate an ideal TSL. + * + * @param AssetGraph $graph + * @param AssetGraph $transpose + * + * @return \SplQueue $queue + * A queue of vertices + */ + protected function createSourceQueue(AssetGraph $graph, AssetGraph $transpose) { + $reach_visitor = new DepthFirstBasicVisitor(); + + // Find source vertices (outdegree 0) in the original graph + $sources = DepthFirst::find_sources($graph, $reach_visitor); + + // Traverse the transposed graph to get reachability data on each vertex + DepthFirst::traverse($transpose, $reach_visitor, clone $sources); + + // Sort vertices via a PriorityQueue based on total reach + $pq = new \SplPriorityQueue(); + foreach ($sources as $vertex) { + $pq->insert($vertex, count($reach_visitor->getReachable($vertex))); + } + + // Dump the priority queue into a normal queue + // TODO maybe gliph should support pq/heaps as a queue type on which to operate? + $queue = new \SplQueue(); + foreach ($pq as $vertex) { + $queue->push($vertex); + } + + return $queue; + } + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php new file mode 100644 index 0000000..d35d3f9 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerNouveaux.php @@ -0,0 +1,72 @@ +grouper = $grouper; + $this->optimizer = $optimizer; + $this->dumper = $dumper; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function optimize(array $assets) { + $tsl = $this->grouper->group($assets); + } + + +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php new file mode 100644 index 0000000..ed651bf --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Exception/AssetTypeMismatchException.php @@ -0,0 +1,16 @@ + array( + 'file' => 'Drupal\\Core\\Asset\\StylesheetFileAsset', + 'external' => 'Drupal\\Core\\Asset\\StylesheetExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\StylesheetStringAsset', + ), + 'js' => array( + 'file' => 'Drupal\\Core\\Asset\\JavascriptFileAsset', + 'external' => 'Drupal\\Core\\Asset\\JavascriptExternalAsset', + 'string' => 'Drupal\\Core\\Asset\\JavascriptStringAsset', + ), + ); + + public function __construct(AssetBagInterface $bag = NULL) { + $this->restoreDefaults(); + + if (!is_null($bag)) { + $this->setBag($bag); + } + } + + /** + * Adds an asset to the contained AssetBag. + * + * It is not necessary to call this method on assets that were created via the + * create() method. + * + * @param AssetInterface $asset + * The asset to add to the contained bag. + */ + public function add(AssetInterface $asset) { + if (empty($this->bag)) { + throw new \Exception('No bag is currently attached to this collector.'); + } + $this->bag->add($asset); + return $this; + } + + /** + * Creates an asset, stores it in the collector's bag, and returns it. + * + * TODO flesh out these docs to be equivalent to drupal_add_css/js() + * + * @param string $asset_type + * A string indicating the asset type - 'css' or 'js'. + * @param string $source_type + * A string indicating the source type - 'file', 'external' or 'string'. + * @param string $data + * A string containing data that defines the asset. Appropriate values vary + * depending on the source_type param: + * - 'file': the relative path to the file, or a stream wrapper URI. + * - 'external': the absolute path to the external asset. + * - 'string': a string containing valid CSS or Javascript to be injected + * directly onto the page. + * @param array $options + * An array of metadata to explicitly set on the asset. These will override + * metadata defaults that are injected onto the asset at creation time. + * @param array $filters + * An array of filters to apply to the object + * TODO this should, maybe, be removed entirely + * + * @return \Drupal\Core\Asset\AssetInterface + * + * @throws \InvalidArgumentException + * Thrown if an invalid asset type or source type is passed. + */ + public function create($asset_type, $source_type, $data, $options = array(), $filters = array()) { + if (!isset($this->classMap[$asset_type])) { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are allowed, "%s" requested.', $asset_type)); + } + if (!isset($this->classMap[$asset_type][$source_type])) { + throw new \InvalidArgumentException(sprintf('Only sources of type "file", "string", or "external" are allowed, "%s" requested.', $source_type)); + } + + $metadata = $this->getMetadataDefaults($asset_type); + $metadata->replace($options); + + $class = $this->classMap[$asset_type][$source_type]; + $asset = new $class($metadata, $data, $filters); + + if (!empty($this->bag)) { + $this->add($asset); + } + + return $asset; + } + + public function setBag(AssetBagInterface $bag) { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. A new bag cannot be attached to a locked collector.'); + } + $this->bag = $bag; + } + + public function clearBag() { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. Bags cannot be cleared on a locked collector.'); + } + $this->bag = NULL; + } + + public function createJavascriptSetting() { + // TODO figure out settings + } + + public function lock($key) { + if ($this->isLocked()) { + throw new \Exception('Collector is already locked.', E_WARNING); + } + + $this->locked = TRUE; + $this->lockKey = $key; + return TRUE; + } + + public function unlock($key) { + if (!$this->isLocked()) { + throw new \Exception('Collector is not locked', E_WARNING); + } + + if ($this->lockKey !== $key) { + throw new \Exception('Attempted to unlock Collector with incorrect key.', E_WARNING); + } + + $this->locked = FALSE; + $this->lockKey = NULL; + return TRUE; + } + + public function isLocked() { + return $this->locked; + } + + public function setDefaultMetadata($type, AssetMetadataBag $metadata) { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + + if ($type === 'css') { + $this->defaultCssMetadata = $metadata; + } + elseif ($type === 'js') { + $this->defaultJsMetadata = $metadata; + } + else { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $type)); + } + } + + /** + * Gets a clone of the metadata bag for a given asset type. + * + * Clones are returned in order to ensure there is a unique metadata object + * for every asset, and that the default metadata contained in the collector + * cannot be modified externally. + * + * @param $type + * A string, 'css' or 'js', indicating the type of metadata to retrieve. + * + * @return AssetMetadataBag + * + * @throws \InvalidArgumentException + * Thrown if a type other than 'css' or 'js' is provided. + */ + public function getMetadataDefaults($type) { + if ($type === 'css') { + return clone $this->defaultCssMetadata; + } + elseif ($type === 'js') { + return clone $this->defaultJsMetadata; + } + else { + throw new \InvalidArgumentException(sprintf('Only assets of type "js" or "css" are supported, "%s" requested.', $type)); + } + } + + /** + * Restores metadata default bags to their default state. + * + * This simply creates new instances of CssMetadataBag and JsMetadataBag, as + * those classes have the normal defaults as hardmapped properties. + * + * @throws \Exception + * Thrown if the collector is locked when this method is called. + */ + public function restoreDefaults() { + if ($this->isLocked()) { + throw new \Exception('The collector instance is locked. Asset defaults cannot be modified on a locked collector.'); + } + $this->defaultCssMetadata = new CssMetadataBag(); + $this->defaultJsMetadata = new JsMetadataBag(); + } +} diff --git a/core/lib/Drupal/Core/Asset/Factory/AssetLibraryCollector.php b/core/lib/Drupal/Core/Asset/Factory/AssetLibraryCollector.php new file mode 100644 index 0000000..84df9ca --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Factory/AssetLibraryCollector.php @@ -0,0 +1,97 @@ +manager = $manager; + } + + public function add($name, AssetLibrary $library) { + $this->manager->add($this->module, $name, $library); + return $this; + } + + public function buildLibrary($name, $values) { + $library = $this->createLibrary($name, $values); + + $collector = new AssetCollector(); + $collector->setBag($library); + $collector->setDefaultMetadata('js', new JsMetadataBag(array('group' => JS_LIBRARY))); + $collector->lock($this->getPrivateKey()); // TODO is locking here a bad idea? + + return $collector; + } + + public function createLibrary($name, $values) { + $library = new AssetLibrary($values); + $this->add($name, $library); + + return $library; + } + + public function setModule($module) { + $this->module = $module; + } + + public function lock($key) { + if ($this->isLocked()) { + throw new \Exception('Collector is already locked.', E_WARNING); + } + + $this->locked = TRUE; + $this->lockKey = $key; + return TRUE; + } + + public function unlock($key) { + if (!$this->isLocked()) { + throw new \Exception('Collector is not locked', E_WARNING); + } + + if ($this->lockKey !== $key) { + throw new \Exception('Attempted to unlock Collector with incorrect key.', E_WARNING); + } + + $this->locked = FALSE; + $this->lockKey = NULL; + return TRUE; + } + + public function isLocked() { + return $this->locked; + } + + protected function getPrivateKey() { + if (empty($this->privateKey)) { + // This doesn't need to be highly secure, just decently random. + $this->privateKey = Crypt::randomStringHashed(8); + } + return $this->privateKey; + } + +} diff --git a/core/lib/Drupal/Core/Asset/JavascriptAssetInterface.php b/core/lib/Drupal/Core/Asset/JavascriptAssetInterface.php new file mode 100644 index 0000000..01f4930 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JavascriptAssetInterface.php @@ -0,0 +1,28 @@ +scope = $scope; + } + + public function getScope() { + return empty($this->scope) ? $this->scopeDefault : $this->scope; + } + + public function getScopeDefault() { + return $this->scopeDefault; + } +} diff --git a/core/lib/Drupal/Core/Asset/JavascriptFileAsset.php b/core/lib/Drupal/Core/Asset/JavascriptFileAsset.php new file mode 100644 index 0000000..3235e14 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JavascriptFileAsset.php @@ -0,0 +1,39 @@ +scope = $scope; + } + + public function getScope() { + return empty($this->scope) ? $this->scopeDefault : $this->scope; + } + + public function getScopeDefault() { + return $this->scopeDefault; + } +} diff --git a/core/lib/Drupal/Core/Asset/JavascriptStringAsset.php b/core/lib/Drupal/Core/Asset/JavascriptStringAsset.php new file mode 100644 index 0000000..c8f3767 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JavascriptStringAsset.php @@ -0,0 +1,38 @@ +scope = $scope; + } + + public function getScope() { + return empty($this->scope) ? $this->scopeDefault : $this->scope; + } + + public function getScopeDefault() { + return $this->scopeDefault; + } +} diff --git a/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php new file mode 100644 index 0000000..963f7e8 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/AssetMetadataBag.php @@ -0,0 +1,95 @@ +default = array_replace_recursive($this->default, $default); + } + + public function all() { + return array_replace_recursive($this->default, $this->explicit); + } + + public function keys() { + return array_keys($this->all()); + } + + public function has($key) { + return array_key_exists($key, $this->explicit) || + array_key_exists($key, $this->default); + } + + /** + * Reverts the a back to its default, if one exists. + * + * @param $key + * + * @return void + */ + public function revert($key) { + unset($this->explicit[$key]); + } + + public function isDefault($key) { + return !array_key_exists($key, $this->explicit) && + array_key_exists($key, $this->default); + } + + public function add(array $values = array()) { + $this->explicit = array_replace_recursive($this->explicit, $values); + } + + public function replace(array $values = array()) { + $this->explicit = $values; + } + + public function get($key) { + if (array_key_exists($key, $this->explicit)) { + return $this->explicit[$key]; + } + + if (array_key_exists($key, $this->default)) { + return $this->default[$key]; + } + } + + public function getIterator() { + return new \ArrayIterator($this->all()); + } + + public function count() { + return count($this->all()); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php new file mode 100644 index 0000000..1f7c410 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/CssMetadataBag.php @@ -0,0 +1,30 @@ + CSS_AGGREGATE_DEFAULT, // TODO Just removing this would be *awesome*. + 'every_page' => FALSE, + 'media' => 'all', + 'preprocess' => TRUE, + 'browsers' => array( + 'IE' => TRUE, + '!IE' => TRUE, + ), + ); + + public function __construct(array $default = array()) { + $this->default = array_replace_recursive($this->default, $default); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php new file mode 100644 index 0000000..fde88e2 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Metadata/JsMetadataBag.php @@ -0,0 +1,30 @@ + JS_DEFAULT, + 'every_page' => FALSE, + 'scope' => 'header', + 'cache' => TRUE, + 'preprocess' => TRUE, + 'attributes' => array(), + 'version' => NULL, + 'browsers' => array(), + ); + + public function __construct(array $default = array()) { + $this->default = array_replace_recursive($this->default, $default); + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php b/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php new file mode 100644 index 0000000..cc77d0e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/OptimallyGroupedTSLVisitor.php @@ -0,0 +1,111 @@ +tsl = array(); + $this->groups = $groups; + $this->vertexMap = $vertex_map; + } + + /** + * {@inheritdoc} + */ + public function onInitializeVertex($vertex, $source, \SplQueue $queue) {} + + /** + * {@inheritdoc} + */ + public function onBackEdge($vertex, \Closure $visit) { + // TODO: Implement onBackEdge() method. + } + + /** + * {@inheritdoc} + */ + public function onStartVertex($vertex, \Closure $visit) { + $this->active->attach($vertex); + + // If there's a record in the vertex map, it means this vertex has an + // optimal group. Remove it from that group, as it being here means it's + // been visited. + if ($this->vertexMap->contains($vertex)) { + $this->vertexMap[$vertex]->detach($vertex); + } + } + + /** + * {@inheritdoc} + */ + public function onExamineEdge($from, $to, \Closure $visit) {} + + /** + * Here be the unicorns. + * + * Once the depth-first traversal is done for a vertex, rather than + * simply pushing it onto the TSL and moving on (as in a basic depth-first + * traversal), if the finished vertex is a member of an optimality group, then + * visit all other (unvisited) members of that optimality group. + * + * This ensures the final TSL has the tightest possible adherence to the + * defined optimal groupings while still respecting the DAG. + * + */ + public function onFinishVertex($vertex, \Closure $visit) { + // TODO this still isn't quite optimal; it can split groups unnecessarily. tweak a little more. + // TODO explore risk of hitting the 100 call stack limit + if ($this->vertexMap->contains($vertex)) { + foreach ($this->vertexMap[$vertex] as $vertex) { + $visit($vertex); + } + } + $this->tsl[] = $vertex; + } + + /** + * Returns the TSL produced by a depth-first traversal. + * + * @return array + * A topologically sorted list of vertices. + */ + public function getTSL() { + return $this->tsl; + } +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Asset/StylesheetAssetInterface.php b/core/lib/Drupal/Core/Asset/StylesheetAssetInterface.php new file mode 100644 index 0000000..9a81daf --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StylesheetAssetInterface.php @@ -0,0 +1,40 @@ +media; + } + + /** + * Returns the default value of the media property on this stylesheet asset. + * + * @return mixed + */ + public function getMediaDefault() { + return $this->mediaDefault; + } + + /** + * Sets the media property to be applied on this stylesheet asset. + * + * @param string $type + * Either a media type, or a media query. + * + * @return NULL + */ + public function setMedia($type) { + $this->media = $type; + } +} diff --git a/core/lib/Drupal/Core/Asset/StylesheetFileAsset.php b/core/lib/Drupal/Core/Asset/StylesheetFileAsset.php new file mode 100644 index 0000000..38b3a6b --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StylesheetFileAsset.php @@ -0,0 +1,55 @@ +media; + } + + /** + * Returns the default value of the media property on this stylesheet asset. + * + * @return mixed + */ + public function getMediaDefault() { + return $this->mediaDefault; + } + + /** + * Sets the media property to be applied on this stylesheet asset. + * + * @param string $type + * Either a media type, or a media query. + * + * @return NULL + */ + public function setMedia($type) { + $this->media = $type; + } +} diff --git a/core/lib/Drupal/Core/Asset/StylesheetStringAsset.php b/core/lib/Drupal/Core/Asset/StylesheetStringAsset.php new file mode 100644 index 0000000..639f6c1 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/StylesheetStringAsset.php @@ -0,0 +1,57 @@ +media; + } + + /** + * Returns the default value of the media property on this stylesheet asset. + * + * @return mixed + */ + public function getMediaDefault() { + return $this->mediaDefault; + } + + /** + * Sets the media property to be applied on this stylesheet asset. + * + * @param string $type + * Either a media type, or a media query. + * + * @return NULL + */ + public function setMedia($type) { + $this->media = $type; + } +} diff --git a/core/modules/block/lib/Drupal/block/BlockBase.php b/core/modules/block/lib/Drupal/block/BlockBase.php index 579e841..1768991 100644 --- a/core/modules/block/lib/Drupal/block/BlockBase.php +++ b/core/modules/block/lib/Drupal/block/BlockBase.php @@ -11,6 +11,7 @@ use Drupal\block\BlockInterface; use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\Language; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines a base block implementation that most blocks plugins will extend. @@ -181,5 +182,10 @@ public function getMachineNameSuggestion() { return $transliterated; } - + /** + * {@inheritdoc} + */ + public function declareAssets(AssetCollector $collector) {} } + + diff --git a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php index b5433e6..d625616 100644 --- a/core/modules/block/lib/Drupal/block/BlockPluginInterface.php +++ b/core/modules/block/lib/Drupal/block/BlockPluginInterface.php @@ -10,6 +10,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface; use Drupal\Core\Plugin\PluginFormInterface; +use Drupal\Core\Asset\Factory\AssetCollector; /** * Defines the required interface for all block plugins. @@ -122,4 +123,12 @@ public function blockSubmit($form, &$form_state); */ public function getMachineNameSuggestion(); + /** + * Declares the assets required by this block to a collector. + * + * @param \Drupal\Core\Asset\Factory\AssetCollector $collector + * + * @return void + */ + public function declareAssets(AssetCollector $collector); } diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetAssemblyTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetAssemblyTest.php new file mode 100644 index 0000000..bb3c6ab --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetAssemblyTest.php @@ -0,0 +1,87 @@ + 'Asset assembly tests', + 'description' => 'Tests to ensure assets declared via the various possible approaches come out with the correct properties, in the proper order.', + 'group' => 'Asset', + ); + } + + public function createJQueryAssetLibrary() { + $library = new AssetLibrary(array(new JavascriptFileAsset(new JsMetadataBag(), 'core/misc/jquery.js'))); + return $library->setTitle('jQuery') + ->setVersion('1.8.2') + ->setWebsite('http://jquery.com'); + } + + /** + * Tests various simple single-bag asset assembly scenarios. + * + * Much of the real complexity of asset ordering in AssetBags comes from + * nesting them, but these tests are focused on the basic mechanics of + * assembly within a single bag. + */ + public function testSingleBagAssetAssemblies() { + // Dead-simple bag - contains just one css and one js assets, both local files. + $bag = new AssetBag(); + + $css1 = new StylesheetFileAsset(new CssMetadataBag(), 'foo'); + $js1 = new JavascriptFileAsset(new JsMetadataBag(), 'baz'); + + $bag->add($css1); + $bag->add($js1); + + $this->assertTrue($bag->hasCss(), 'AssetBag correctly reports that it contains CSS assets.'); + $this->assertTrue($bag->hasJs(), 'AssetBag correctly reports that it contains javascript assets.'); + + $css_collection = new CssCollection(); + $css_collection->add($css1); + + $js_collection = new JsCollection(); + $js_collection->add($js1); + + $this->assertEquals($css_collection, $bag->getCss()); + $this->assertEquals($js_collection, $bag->getJs()); + + $css2 = new StylesheetFileAsset(new CssMetadataBag(), 'bing'); + $bag->add($css2); + $css_collection->add($css2); + + $this->assertEquals($css_collection, $bag->getCss()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetBagTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetBagTest.php new file mode 100644 index 0000000..5d5ea1e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetBagTest.php @@ -0,0 +1,62 @@ + 'Asset bag unit tests', + 'description' => 'Unit tests on AssetBag', + 'group' => 'Asset', + ); + } + + public function testAddValidAsset() { + // Dead-simple bag - contains just one css and one js assets, both local files. + $bag = new AssetBag(); + + $css1 = $this->getMock('Drupal\\Core\\Asset\\StylesheetFileAsset', array(), array(), '', FALSE); + $js1 = $this->getMock('Drupal\\Core\\Asset\\JavascriptFileAsset', array(), array(), '', FALSE); + + $bag->add($css1); + $bag->add($js1); + + $this->assertTrue($bag->hasCss(), 'AssetBag correctly reports that it contains CSS assets.'); + $this->assertTrue($bag->hasJs(), 'AssetBag correctly reports that it contains javascript assets.'); + + $css_collection = new CssCollection(); + $css_collection->add($css1); + + $js_collection = new JsCollection(); + $js_collection->add($js1); + + $this->assertEquals($css_collection, $bag->getCss()); + $this->assertEquals($js_collection, $bag->getJs()); + + $css2 = $this->getMock('Drupal\\Core\\Asset\\StylesheetFileAsset', array(), array(), '', FALSE); + + $bag->add($css2); + $css_collection->add($css2); + + $this->assertEquals($css_collection, $bag->getCss()); + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetCollectorTest.php new file mode 100644 index 0000000..20d4535 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetCollectorTest.php @@ -0,0 +1,229 @@ + 'Asset Collector tests', + 'description' => 'Tests that the AssetCollector system works correctly.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + $this->collector = new AssetCollector(); + } + + /** + * Tests that the collector injects provided metadata to created assets. + */ + public function testMetadataInjection() { + $asset = $this->collector->create('css', 'file', 'foo', array('group' => CSS_AGGREGATE_THEME)); + $meta = $asset->getMetadata(); + $this->assertEquals(CSS_AGGREGATE_THEME, $meta->get('group'), 'Collector injected user-passed parameters into the created asset.'); + $this->assertFalse($meta->isDefault('group')); + } + + public function testDefaultPropagation() { + // Test that defaults are correctly applied by the factory. + $meta = new CssMetadataBag(array('every_page' => TRUE, 'group' => CSS_AGGREGATE_THEME)); + $this->collector->setDefaultMetadata('css', $meta); + $css1 = $this->collector->create('css', 'file', 'foo'); + + $asset_meta = $css1->getMetadata(); + $this->assertTrue($asset_meta->get('every_page')); + $this->assertEquals(CSS_AGGREGATE_THEME, $asset_meta->get('group')); + } + + /** + * @expectedException Exception + */ + public function testExceptionOnAddingAssetWithoutBagPresent() { + $asset = $this->collector->create('css', 'string', 'foo'); + $this->collector->add($asset); + } + + /** + * TODO separate test for an explicit add() call. + */ + public function testAssetsImplicitlyArriveInInjectedBag() { + $bag = new AssetBag(); + $this->collector->setBag($bag); + + $asset = $this->collector->create('css', 'file', 'bar'); + $this->assertContains($asset, $bag->getCss(), 'Created asset was implicitly added to bag.'); + } + + public function testAddAssetExplicitly() { + $bag = new AssetBag(); + $this->collector->setBag($bag); + + $asset = $this->getMock('Drupal\\Core\\Asset\\StylesheetFileAsset', array(), array(), '', FALSE); + $this->collector->add($asset); + + $this->assertContains($asset, $bag->getCss()); + } + + /** + * @expectedException Exception + */ + public function testClearBag() { + $bag = new AssetBag(); + $this->collector->setBag($bag); + $this->collector->clearBag(); + + $this->collector->add($this->collector->create('css', 'file', 'bar')); + } + + public function testLock() { + $this->assertTrue($this->collector->lock($this), 'Collector locked successfully.'); + $this->assertTrue($this->collector->isLocked(), 'Collector accurately reports that it is locked via isLocked() method.'); + } + + public function testUnlock() { + $this->collector->lock($this); + $this->assertTrue($this->collector->unlock($this), 'Collector unlocked successfully when appropriate key was provided.'); + $this->assertFalse($this->collector->isLocked(), 'Collector correctly reported unlocked state via isLocked() method after unlocking.'); + } + + /** + * @expectedException Exception + */ + public function testUnlockFailsWithoutCorrectSecret() { + $this->collector->lock('foo'); + $this->collector->unlock('bar'); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsSettingDefaults() { + $this->collector->lock($this); + $this->collector->setDefaultMetadata('css', new CssMetadataBag()); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsRestoringDefaults() { + $this->collector->lock($this); + $this->collector->restoreDefaults(); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsClearingBag() { + $this->collector->lock($this); + $this->collector->clearBag(); + } + + /** + * @expectedException Exception + */ + public function testLockingPreventsSettingBag() { + $this->collector->lock($this); + $this->collector->setBag(new AssetBag()); + } + + public function testBuiltinDefaultAreTheSame() { + $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css')); + $this->assertEquals(new JsMetadataBag(), $this->collector->getMetadataDefaults('js')); + } + + public function testChangeAndRestoreDefaults() { + $changed_css = new CssMetadataBag(array('foo' => 'bar', 'every_page' => TRUE)); + $this->collector->setDefaultMetadata('css', $changed_css); + + $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css')); + $this->assertNotSame($changed_css, $this->collector->getMetadataDefaults('css'), 'Metadata is cloned on retrieval from collector.'); + + $this->collector->restoreDefaults(); + $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css')); + + // Do another check to ensure that both metadata bags are correctly reset + $changed_js = new JsMetadataBag(array('scope' => 'footer', 'fizzbuzz' => 'llama')); + $this->collector->setDefaultMetadata('css', $changed_css); + $this->collector->setDefaultMetadata('js', $changed_js); + + $this->assertEquals($changed_css, $this->collector->getMetadataDefaults('css')); + $this->assertEquals($changed_js, $this->collector->getMetadataDefaults('js')); + + $this->collector->restoreDefaults(); + $this->assertEquals(new CssMetadataBag(), $this->collector->getMetadataDefaults('css')); + $this->assertEquals(new JsMetadataBag(), $this->collector->getMetadataDefaults('js')); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testGetNonexistentDefault() { + $this->collector->getMetadataDefaults('foo'); + } + + + public function testCreateStylesheetFileAsset() { + $css_file1 = $this->collector->create('css', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StylesheetFileAsset', $css_file1, 'Collector correctly created a StylesheetFileAsset instance.'); + } + + public function testCreateStylesheetExternalAsset() { + $css_external1 = $this->collector->create('css', 'external', 'http://foo.bar/path/to/asset.css'); + $this->assertInstanceOf('\Drupal\Core\Asset\StylesheetExternalAsset', $css_external1, 'Collector correctly created a StylesheetExternalAsset instance.'); + } + + public function testCreateStylesheetStringAsset() { + $css_string1 = $this->collector->create('css', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\StylesheetStringAsset', $css_string1, 'Collector correctly created a StylesheetStringAsset instance .'); + } + + public function testCreateJavascriptFileAsset() { + $js_file1 = $this->collector->create('js', 'file', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\JavascriptFileAsset', $js_file1, 'Collector correctly created a JavascriptFileAsset instance .'); + } + + public function testCreateJavascriptExternalAsset() { + $js_external1 = $this->collector->create('js', 'external', 'http://foo.bar/path/to/asset.js'); + $this->assertInstanceOf('\Drupal\Core\Asset\JavascriptExternalAsset', $js_external1, 'Collector correctly created a JavascriptExternalAsset instance .'); + } + + public function testCreateJavascriptStringAsset() { + $js_string1 = $this->collector->create('js', 'string', 'foo'); + $this->assertInstanceOf('\Drupal\Core\Asset\JavascriptStringAsset', $js_string1, 'Collector correctly created a JavascriptStringAsset instance .'); + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetLibraryManagerTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryManagerTest.php new file mode 100644 index 0000000..59a7b11 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryManagerTest.php @@ -0,0 +1,31 @@ + '', // TODO give me a name + 'description' => '', // TODO give me a description + 'group' => '', // TODO give me the same group as above + ); + } + + public function setUp() { + parent::setUp(); + } + + public function testStub() { + // TODO anything. without this, phpunit blows up. + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetLibraryTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryTest.php new file mode 100644 index 0000000..3be2f3d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetLibraryTest.php @@ -0,0 +1,96 @@ + 'Asset Library tests', + 'description' => 'Tests that the AssetLibrary behaves correctly.', + 'group' => 'Asset', + ); + } + + public function getLibraryFixture() { + $library = new AssetLibrary(); + $library->setTitle('foo') + ->setVersion('1.2.3') + ->setWebsite('http://foo.bar') + ->addDependency('foo', 'bar'); + return $library; + } + + public function testConstructorValueInjection() { + $values = array( + 'title' => 'foo', + 'version' => '1.2.3', + 'website' => 'http://foo.bar', + 'dependencies' => array(array('foo', 'bar')), + ); + $library = new AssetLibrary($values); + + $fixture = $this->getLibraryFixture(); + $this->assertEquals($fixture->getTitle(), $library->getTitle(), 'Title passed correctly through the constructor.'); + $this->assertEquals($fixture->getVersion(), $library->getVersion(), 'Version passed correctly through the constructor.'); + $this->assertEquals($fixture->getWebsite(), $library->getWebsite(), 'Website passed correctly through the constructor.'); + $this->assertEquals($fixture->getDependencyInfo(), $library->getDependencyInfo(), 'Dependencies information passed correctly through the constructor.'); + } + + public function testAddDependency() { + $library = $this->getLibraryFixture(); + $library->addDependency('baz', 'bing'); + $this->assertEquals($library->getDependencyInfo(), array(array('foo', 'bar'), array('baz', 'bing')), 'Dependencies added to library successfully.'); + } + + public function testClearDependencies() { + $library = $this->getLibraryFixture(); + $library->clearDependencies(); + $this->assertEmpty($library->getDependencyInfo(), 'Dependencies recorded in the library were cleared correctly.'); + } + + public function testFrozenNonwriteability() { + $library = $this->getLibraryFixture(); + $library->freeze(); + try { + $library->setTitle('bar'); + $this->fail('No exception thrown when attempting to set a new title on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->setVersion('2.3.4'); + $this->fail('No exception thrown when attempting to set a new version on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->setWebsite('http://bar.baz'); + $this->fail('No exception thrown when attempting to set a new website on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->addDependency('bing', 'bang'); + $this->fail('No exception thrown when attempting to add a new dependency on a frozen library.'); + } + catch (\LogicException $e) {} + + try { + $library->clearDependencies(); + $this->fail('No exception thrown when attempting to clear dependencies from a frozen library.'); + } + catch (\LogicException $e) {} + } +} diff --git a/core/tests/Drupal/Tests/Core/Asset/AssetTest.php b/core/tests/Drupal/Tests/Core/Asset/AssetTest.php new file mode 100644 index 0000000..2752d8c --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/AssetTest.php @@ -0,0 +1,37 @@ + 'Asset tests', + 'description' => 'Unit tests for all base asset classes.', + 'group' => 'Asset', + ); + } + + public function setUp() { + parent::setUp(); + } + + public function testStub() { + // TODO anything. without this, phpunit blows up. + } +} diff --git a/core/vendor/autoload.php b/core/vendor/autoload.php index a391d4e..a525121 100644 --- a/core/vendor/autoload.php +++ b/core/vendor/autoload.php @@ -4,4 +4,4 @@ require_once __DIR__ . '/composer' . '/autoload_real.php'; -return ComposerAutoloaderInit0b93b0210b8b39c2a0b13410cd082de9::getLoader(); +return ComposerAutoloaderInit7816b7cb10809286c097e9dcbf7023e2::getLoader(); diff --git a/core/vendor/composer/autoload_namespaces.php b/core/vendor/composer/autoload_namespaces.php index 926818b..f6dbb7c 100644 --- a/core/vendor/composer/autoload_namespaces.php +++ b/core/vendor/composer/autoload_namespaces.php @@ -28,6 +28,7 @@ 'Guzzle\\Parser' => array($vendorDir . '/guzzle/parser'), 'Guzzle\\Http' => array($vendorDir . '/guzzle/http'), 'Guzzle\\Common' => array($vendorDir . '/guzzle/common'), + 'Gliph' => array($vendorDir . '/sdboyer/gliph/src'), 'EasyRdf_' => array($vendorDir . '/easyrdf/easyrdf/lib'), 'Drupal\\Driver' => array($baseDir . '/drivers/lib'), 'Drupal\\Core' => array($baseDir . '/core/lib'), diff --git a/core/vendor/composer/autoload_real.php b/core/vendor/composer/autoload_real.php index 6bb9081..52043fb 100644 --- a/core/vendor/composer/autoload_real.php +++ b/core/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit0b93b0210b8b39c2a0b13410cd082de9 +class ComposerAutoloaderInit7816b7cb10809286c097e9dcbf7023e2 { private static $loader; @@ -19,9 +19,9 @@ public static function getLoader() return self::$loader; } - spl_autoload_register(array('ComposerAutoloaderInit0b93b0210b8b39c2a0b13410cd082de9', 'loadClassLoader'), true, true); + spl_autoload_register(array('ComposerAutoloaderInit7816b7cb10809286c097e9dcbf7023e2', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); - spl_autoload_unregister(array('ComposerAutoloaderInit0b93b0210b8b39c2a0b13410cd082de9', 'loadClassLoader')); + spl_autoload_unregister(array('ComposerAutoloaderInit7816b7cb10809286c097e9dcbf7023e2', 'loadClassLoader')); $vendorDir = dirname(__DIR__); $baseDir = dirname(dirname($vendorDir)); diff --git a/core/vendor/composer/installed.json b/core/vendor/composer/installed.json index f5d9d72..be27d13 100644 --- a/core/vendor/composer/installed.json +++ b/core/vendor/composer/installed.json @@ -2064,5 +2064,51 @@ ], "description": "Symfony Process Component", "homepage": "http://symfony.com" + }, + { + "name": "sdboyer/gliph", + "version": "0.1.1", + "version_normalized": "0.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sdboyer/gliph.git", + "reference": "c7bd13eb2e6b51b017f025e24a7a676a4eed1a4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sdboyer/gliph/zipball/c7bd13eb2e6b51b017f025e24a7a676a4eed1a4a", + "reference": "c7bd13eb2e6b51b017f025e24a7a676a4eed1a4a", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "time": "2013-09-14 04:16:01", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Gliph": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sam Boyer", + "email": "tech@samboyer.org" + } + ], + "description": "A graph library for PHP.", + "homepage": "http://github.com/sdboyer/gliph", + "keywords": [ + "gliph", + "graph", + "library", + "php", + "spl" + ] } ] diff --git a/core/vendor/sdboyer/gliph/README.md b/core/vendor/sdboyer/gliph/README.md new file mode 100644 index 0000000..4415ef0 --- /dev/null +++ b/core/vendor/sdboyer/gliph/README.md @@ -0,0 +1,38 @@ +# Gliph + +[![Build Status](https://travis-ci.org/sdboyer/gliph.png?branch=php53)](https://travis-ci.org/sdboyer/gliph) +[![Latest Stable Version](https://poser.pugx.org/sdboyer/gliph/v/stable.png)](https://packagist.org/packages/sdboyer/gliph) + +Gliph is a **g**raph **li**brary for **PH**P. It provides graph building blocks and datastructures for use by other PHP applications. It is (currently) designed for use with in-memory graphs, not for interaction with a graph database like [Neo4J](http://neo4j.org/). + +Gliph is designed with performance in mind, but primarily to provide a sane interface. Graphs are hard enough without an arcane API making it worse. + +## Core Concepts + +Gliph has several components that work together: graph classes, algorithms, and visitors. Generally speaking, Gliph is patterned after the [C++ Boost Graph Library](http://www.boost.org/libs/graph/doc); reading their documentation can yield a lot of insight into how Gliph is intended to work. + +Note that Gliph is currently written for compatibility with PHP 5.3, but it is intended to port the library to PHP 5.5. The availability of traits, non-scalar/object keys returnable from iterators, and generators will considerably change both the internal and public-facing implementations. + +### Graphs + +There are a number of different strategies for representing graphs; these strategies are more or less efficient depending on certain properties the graph, and what needs to be done to the graph. The approach taken in Gliph is to offer a roughly consistent 'Graph' interface that is common to all these different strategies. The strategies will have varying levels of efficiency at meeting this common interface, so it is the responsibility of the user to select a graph implementation that is appropriate for their use case. This approach draws heavily from the [taxonomy of graphs](http://www.boost.org/doc/libs/1_54_0/libs/graph/doc/graph_concepts.html) established by the BGL. + +Gliph currently implements only an adjacency list graph strategy, in both directed and undirected flavors. Adjacency lists offer efficient access to out-edges, but inefficient access to in-edges (in a directed graph - in an undirected graph, in-edges and out-edges are the same). Adjacency lists and are generally more space-efficient for sparse graphs. + +## TODOs + +Lots. But, to start with: + +- Port to, or provide a parallel implementation in, PHP 5.5. Generators and non-scalar keys from iterators make this all SO much better. In doing that, also shift as much over to traits as possible. +- Implement a generic breadth-first algorithm and its corresponding visitors. +- Implement a generic iterative deepening depth-first algorithm, and its corresponding visitors. +- Implement other popular connected components algorithms, as well as some shortest path algorithms (starting with Dijkstra) +- Write up some examples showing how to actually use the library. + +## Acknowledgements + +This library draws heavy inspiration from the [C++ Boost Graph Library](http://www.boost.org/libs/graph/doc). + +## License + +MIT diff --git a/core/vendor/sdboyer/gliph/composer.json b/core/vendor/sdboyer/gliph/composer.json new file mode 100644 index 0000000..1434d37 --- /dev/null +++ b/core/vendor/sdboyer/gliph/composer.json @@ -0,0 +1,20 @@ +{ + "name": "sdboyer/gliph", + "description": "A graph library for PHP.", + "license": "MIT", + "keywords": ["gliph", "library", "php", "spl", "graph"], + "homepage": "http://github.com/sdboyer/gliph", + "type": "library", + "authors": [ + { + "name": "Sam Boyer", + "email": "tech@samboyer.org" + } + ], + "require": { + "php": ">=5.3" + }, + "autoload": { + "psr-0": { "Gliph": "src/" } + } +} diff --git a/core/vendor/sdboyer/gliph/composer.lock b/core/vendor/sdboyer/gliph/composer.lock new file mode 100644 index 0000000..bc1108a --- /dev/null +++ b/core/vendor/sdboyer/gliph/composer.lock @@ -0,0 +1,439 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + ], + "hash": "c2c349f17b3e09198ed1a8335e431197", + "packages": [ + + ], + "packages-dev": [ + { + "name": "phpunit/php-code-coverage", + "version": "1.2.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "1.2.12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.12", + "reference": "1.2.12", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": ">=1.3.0@stable", + "phpunit/php-text-template": ">=1.1.1@stable", + "phpunit/php-token-stream": ">=1.1.3@stable" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*@dev" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.0.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2013-07-06 06:26:16" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.3.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "1.3.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-file-iterator/zipball/1.3.3", + "reference": "1.3.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "File/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2012-10-11 04:44:38" + }, + { + "name": "phpunit/php-text-template", + "version": "1.1.4", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/php-text-template.git", + "reference": "1.1.4" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/php-text-template/zipball/1.1.4", + "reference": "1.1.4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "Text/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2012-10-31 11:15:28" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "1.0.5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1.0.5", + "reference": "1.0.5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2013-08-02 07:42:54" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "1.2.0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1.2.0", + "reference": "1.2.0", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2013-08-04 05:57:48" + }, + { + "name": "phpunit/phpunit", + "version": "3.7.24", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "3.7.24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.24", + "reference": "3.7.24", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpunit/php-code-coverage": "~1.2.1", + "phpunit/php-file-iterator": ">=1.3.1", + "phpunit/php-text-template": ">=1.1.1", + "phpunit/php-timer": ">=1.0.4", + "phpunit/phpunit-mock-objects": "~1.2.0", + "symfony/yaml": "~2.0" + }, + "require-dev": { + "pear-pear/pear": "1.9.4" + }, + "suggest": { + "ext-json": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "phpunit/php-invoker": ">=1.1.0,<1.2.0" + }, + "bin": [ + "composer/bin/phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "", + "../../symfony/yaml/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "http://www.phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2013-08-09 06:58:24" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "1.2.3", + "source": { + "type": "git", + "url": "git://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "1.2.3" + }, + "dist": { + "type": "zip", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects/archive/1.2.3.zip", + "reference": "1.2.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-text-template": ">=1.1.1@stable" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHPUnit/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2013-01-13 10:24:48" + }, + { + "name": "symfony/yaml", + "version": "v2.3.3", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "v2.3.3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/v2.3.3", + "reference": "v2.3.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com", + "time": "2013-07-21 12:12:18" + } + ], + "aliases": [ + + ], + "minimum-stability": "stable", + "stability-flags": [ + + ], + "platform": { + "php": ">=5.3" + }, + "platform-dev": [ + + ] +} diff --git a/core/vendor/sdboyer/gliph/phpunit.xml.dist b/core/vendor/sdboyer/gliph/phpunit.xml.dist new file mode 100644 index 0000000..b847773 --- /dev/null +++ b/core/vendor/sdboyer/gliph/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + tests/ + + + + + src/Gliph + + src/Gliph/Visitor/DepthFirstNoOpVisitor.php + + + + + + + + + diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Exception/InvalidVertexTypeException.php b/core/vendor/sdboyer/gliph/src/Gliph/Exception/InvalidVertexTypeException.php new file mode 100644 index 0000000..4b33c81 --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Exception/InvalidVertexTypeException.php @@ -0,0 +1,13 @@ +vertices = new \SplObjectStorage(); + } + + public function addVertex($vertex) { + if (!is_object($vertex)) { + throw new InvalidVertexTypeException('Vertices must be objects; non-object provided.'); + } + + if (!$this->hasVertex($vertex)) { + $this->vertices[$vertex] = new \SplObjectStorage(); + } + } + + public function eachAdjacent($vertex, $callback) { + foreach ($this->vertices[$vertex] as $e) { + call_user_func($callback, $e); + } + } + + public function eachVertex($callback) { + $this->fev(function ($v, $outgoing) use ($callback) { + call_user_func($callback, $v, $outgoing); + }); + } + + public function hasVertex($vertex) { + return $this->vertices->contains($vertex); + } + + protected function fev($callback) { + foreach ($this->vertices as $vertex) { + $outgoing = $this->vertices->getInfo(); + $callback($vertex, $outgoing); + } + } +} \ No newline at end of file diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Graph/DirectedAdjacencyGraph.php b/core/vendor/sdboyer/gliph/src/Gliph/Graph/DirectedAdjacencyGraph.php new file mode 100644 index 0000000..81f0440 --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Graph/DirectedAdjacencyGraph.php @@ -0,0 +1,76 @@ +hasVertex($from)) { + $this->addVertex(($from)); + } + + if (!$this->hasVertex($to)) { + $this->addVertex($to); + } + + $this->vertices[$from]->attach($to); + } + + public function removeVertex($vertex) { + if (!$this->hasVertex($vertex)) { + throw new \OutOfBoundsException('Vertex is not in the graph, it cannot be removed.', E_WARNING); + } + + $this->eachVertex(function($v, $outgoing) use ($vertex) { + if ($outgoing->contains($vertex)) { + $outgoing->detach($vertex); + } + }); + unset($this->vertices[$vertex]); + } + + public function removeEdge($from, $to) { + $this->vertices[$from]->detach($to); + } + + public function eachEdge($callback) { + $edges = array(); + $this->fev(function ($from, $outgoing) use (&$edges) { + foreach ($outgoing as $to) { + $edges[] = array($from, $to); + } + }); + + foreach ($edges as $edge) { + call_user_func($callback, $edge); + } + } + + /** + * Returns the transpose of this graph. + * + * A transpose is identical to the current graph, except that + * its edges have had their directionality reversed. + * + * Also sometimes known as the 'reverse' or 'converse'. + * + * @return \Gliph\Graph\DirectedAdjacencyGraph + */ + public function transpose() { + $graph = new self(); + $this->eachEdge(function($edge) use (&$graph) { + $graph->addDirectedEdge($edge[1], $edge[0]); + }); + + return $graph; + } + + public function getCycles() { + $tarjan = new Tarjan(); + $scc = $tarjan->getCycles($this); + return $scc->count() > 0 ? $scc : FALSE; + } +} + diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Graph/UndirectedAdjacencyGraph.php b/core/vendor/sdboyer/gliph/src/Gliph/Graph/UndirectedAdjacencyGraph.php new file mode 100644 index 0000000..d9764e2 --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Graph/UndirectedAdjacencyGraph.php @@ -0,0 +1,52 @@ +hasVertex($from)) { + $this->addVertex(($from)); + } + + if (!$this->hasVertex($to)) { + $this->addVertex($to); + } + + $this->vertices[$from]->attach($to); + $this->vertices[$to]->attach($from); + } + + public function removeVertex($vertex) { + if (!$this->hasVertex($vertex)) { + throw new \OutOfBoundsException('Vertex is not in the graph, it cannot be removed.', E_WARNING); + } + + foreach ($this->vertices[$vertex] as $adjacent) { + $this->vertices[$adjacent]->detach($vertex); + } + unset($this->vertices[$vertex]); + } + + public function removeEdge($from, $to) { + $this->vertices[$from]->detach($to); + $this->vertices[$to]->detach($from); + } + + public function eachEdge($callback) { + $edges = array(); + $complete = new \SplObjectStorage(); + $this->fev(function ($a, $adjacent) use (&$edges, &$complete) { + foreach ($adjacent as $b) { + if (!$complete->contains($b)) { + $edges[] = array($a, $b); + } + } + $complete->attach($a); + }); + + foreach ($edges as $edge) { + call_user_func($callback, $edge); + } + } +} \ No newline at end of file diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Tarjan.php b/core/vendor/sdboyer/gliph/src/Gliph/Tarjan.php new file mode 100644 index 0000000..9e37db9 --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Tarjan.php @@ -0,0 +1,87 @@ +index = 0; + $this->scc = new \SplQueue(); + $this->stack = array(); + + $this->graph = $graph; + if ($graph->getVertexTypes() == DirectedAdjacencyGraph::OBJECT_VERTICES) { + $this->vertexIndices = new \SplObjectStorage(); + $this->vertexLowLimits = new \SplObjectStorage(); + } + else { + $this->vertexIndices = new HashMap(); + $this->vertexLowLimits = new HashMap(); + } + + $that = $this; + $graph->eachVertex(function($vertex) use (&$that, &$graph) { + if (!$that->vertexIndices->contains($vertex)) { + $that->strongconnect($vertex); + } + }); + + return $this->scc; + } + + public function strongconnect($vertex) { + $this->vertexIndices[$vertex] = $this->index; + $this->vertexLowLimits[$vertex] = $this->index; + $this->index++; + $this->stack[] = $vertex; + + $that = $this; + $this->graph->eachAdjacent($vertex, function($to) use (&$vertex, &$that) { + if (!$that->vertexIndices->contains($to)) { + $that->strongconnect($to); + $ll = min($that->vertexLowLimits[$vertex], $that->vertexLowLimits[$to]); + $that->vertexLowLimits[$vertex] = $ll; + } + // FIXME Tarjan dictates this search should be constant time. ruh roh. + else if (array_search($to, $that->stack, TRUE) !== FALSE) { + $min = min($that->vertexLowLimits[$vertex], $that->vertexIndices[$to]); + $that->vertexLowLimits[$vertex] = $min; + } + }); + + if ($this->vertexIndices[$vertex] == $this->vertexLowLimits[$vertex]) { + $component = new \SplQueue(); + do { + $popped = array_pop($this->stack); + $component->push($popped); + } while ($vertex !== $popped); + + if ($component->count() > 1 || $this->storeNonCycles) { + $this->scc->push($component); + } + } + } +} \ No newline at end of file diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Traversal/DepthFirst.php b/core/vendor/sdboyer/gliph/src/Gliph/Traversal/DepthFirst.php new file mode 100644 index 0000000..3879924 --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Traversal/DepthFirst.php @@ -0,0 +1,103 @@ +push($start); + } + + if ($queue->isEmpty()) { + throw new \RuntimeException('No start vertex or vertices were provided, and no source vertices could be found in the provided graph.', E_WARNING); + } + + $visiting = new \SplObjectStorage(); + $visited = new \SplObjectStorage(); + + $visit = function($vertex) use ($graph, $visitor, &$visit, $visiting, $visited) { + if ($visiting->contains($vertex)) { + $visitor->onBackEdge($vertex, $visit); + } + else if (!$visited->contains($vertex)) { + $visiting->attach($vertex); + + $visitor->onStartVertex($vertex, $visit); + + $graph->eachAdjacent($vertex, function($to) use ($vertex, &$visit, $visitor) { + $visitor->onExamineEdge($vertex, $to, $visit); + $visit($to); + }); + + $visitor->onFinishVertex($vertex, $visit); + + $visiting->detach($vertex); + $visited->attach($vertex); + } + }; + + while (!$queue->isEmpty()) { + $vertex = $queue->shift(); + $visit($vertex); + } + } + + /** + * Finds source vertices in a DirectedAdjacencyGraph, then enqueues them. + * + * @param DirectedAdjacencyGraph $graph + * @param DepthFirstVisitorInterface $visitor + * + * @return \SplQueue + */ + public static function find_sources(DirectedAdjacencyGraph $graph, DepthFirstVisitorInterface $visitor) { + $incomings = new \SplObjectStorage(); + $queue = new \SplQueue(); + + $graph->eachEdge(function ($edge) use (&$incomings) { + if (!isset($incomings[$edge[1]])) { + $incomings[$edge[1]] = new \SplObjectStorage(); + } + $incomings[$edge[1]]->attach($edge[0]); + }); + + // Prime the queue with vertices that have no incoming edges. + $graph->eachVertex(function($vertex) use ($queue, $incomings, $visitor) { + if (!$incomings->contains($vertex)) { + $queue->push($vertex); + // TRUE second param indicates source vertex + $visitor->onInitializeVertex($vertex, TRUE, $queue); + } + else { + $visitor->onInitializeVertex($vertex, FALSE, $queue); + } + }); + + return $queue; + } +} \ No newline at end of file diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Visitor/DepthFirstBasicVisitor.php b/core/vendor/sdboyer/gliph/src/Gliph/Visitor/DepthFirstBasicVisitor.php new file mode 100644 index 0000000..4c409fa --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Visitor/DepthFirstBasicVisitor.php @@ -0,0 +1,94 @@ +active = new \SplObjectStorage(); + $this->paths = new \SplObjectStorage(); + $this->tsl = array(); + } + + public function onBackEdge($vertex, \Closure $visit) { + throw new \RuntimeException(sprintf('Cycle detected in provided graph.')); + } + + public function onInitializeVertex($vertex, $source, \SplQueue $queue) { + $this->paths[$vertex] = array(); + } + + public function onStartVertex($vertex, \Closure $visit) { + $this->active->attach($vertex); + if (!isset($this->paths[$vertex])) { + $this->paths[$vertex] = array(); + } + } + + public function onExamineEdge($from, $to, \Closure $visit) { + foreach ($this->active as $vertex) { + // TODO this check makes this much less efficient - find a better algo + if (!in_array($to, $this->paths[$vertex])) { + $path = $this->paths[$vertex]; + $path[] = $to; + $this->paths[$vertex] = $path; + } + } + } + + public function onFinishVertex($vertex, \Closure $visit) { + $this->active->detach($vertex); + $this->tsl[] = $vertex; + } + + /** + * Returns valid topological sort of the visited graph as an array. + * + * @return array + */ + public function getTsl() { + return $this->tsl; + } + + /** + * Returns a queue of all vertices reachable from the given vertex. + * + * This should only be called after the visitor has been used in a + * depth-first traversal. + * + * @param object $vertex + * A vertex present in the graph for + * + * @return array + * + * @throws \OutOfRangeException + */ + public function getReachable($vertex) { + if (!isset($this->paths[$vertex])) { + throw new \OutOfRangeException('Unknown vertex provided.'); + } + + return $this->paths[$vertex]; + } +} \ No newline at end of file diff --git a/core/vendor/sdboyer/gliph/src/Gliph/Visitor/DepthFirstNoOpVisitor.php b/core/vendor/sdboyer/gliph/src/Gliph/Visitor/DepthFirstNoOpVisitor.php new file mode 100644 index 0000000..3f83d8a --- /dev/null +++ b/core/vendor/sdboyer/gliph/src/Gliph/Visitor/DepthFirstNoOpVisitor.php @@ -0,0 +1,14 @@ +v = array( + 'a' => new TestVertex('a'), + 'b' => new TestVertex('b'), + 'c' => new TestVertex('c'), + 'd' => new TestVertex('d'), + 'e' => new TestVertex('e'), + 'f' => new TestVertex('f'), + 'g' => new TestVertex('g'), + ); + } + + public function doCheckVerticesEqual($vertices, AdjacencyGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->g : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertEquals($vertices, $found); + } + + public function doCheckVertexCount($count, AdjacencyGraph $graph = NULL) { + $found = array(); + $graph = is_null($graph) ? $this->g : $graph; + + $graph->eachVertex(function ($vertex) use (&$found) { + $found[] = $vertex; + }); + + $this->assertCount($count, $found); + } + + /** + * Tests that an exception is thrown if a string vertex is provided. + * + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddStringVertex() { + $this->g->addVertex('a'); + } + + /** + * Tests that an exception is thrown if an integer vertex is provided. + * + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddIntegerVertex() { + $this->g->addVertex(1); + } + + /** + * Tests that an exception is thrown if a float vertex is provided. + * + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddFloatVertex() { + $this->g->addVertex((float) 1); + } + + /** + * Tests that an exception is thrown if an array vertex is provided. + * + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddArrayVertex() { + $this->g->addVertex(array()); + } + + /** + * Tests that an exception is thrown if a resource vertex is provided. + * + * @expectedException \Gliph\Exception\InvalidVertexTypeException + */ + public function testAddResourceVertex() { + $this->g->addVertex(fopen(__FILE__, 'r')); + } + + public function testAddVertex() { + $this->g->addVertex($this->v['a']); + + $this->assertTrue($this->g->hasVertex($this->v['a'])); + $this->doCheckVertexCount(1, $this->g); + } + + public function testAddVertexTwice() { + // Adding a vertex twice should be a no-op. + $this->g->addVertex($this->v['a']); + $this->g->addVertex($this->v['a']); + + $this->assertTrue($this->g->hasVertex($this->v['a'])); + $this->doCheckVertexCount(1, $this->g); + } + + /** + * @expectedException OutOfBoundsException + */ + public function testRemoveNonexistentVertex() { + $this->g->removeVertex($this->v['a']); + } +} diff --git a/core/vendor/sdboyer/gliph/tests/Gliph/Graph/DirectedAdjacencyGraphTest.php b/core/vendor/sdboyer/gliph/tests/Gliph/Graph/DirectedAdjacencyGraphTest.php new file mode 100644 index 0000000..c48c3c2 --- /dev/null +++ b/core/vendor/sdboyer/gliph/tests/Gliph/Graph/DirectedAdjacencyGraphTest.php @@ -0,0 +1,87 @@ +g = new DirectedAdjacencyGraph(); + } + + + public function testAddDirectedEdge() { + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + + $this->doCheckVerticesEqual(array($this->v['a'], $this->v['b']), $this->g); + } + + public function testRemoveVertex() { + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->doCheckVertexCount(2); + + $this->g->removeVertex($this->v['b']); + $this->doCheckVertexCount(1); + + // Ensure that b was correctly removed from a's outgoing edges + $found = array(); + $this->g->eachAdjacent($this->v['a'], function($to) use (&$found) { + $found[] = $to; + }); + + $this->assertEquals(array(), $found); + } + + + public function testRemoveEdge() { + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->doCheckVerticesEqual(array($this->v['a'], $this->v['b']), $this->g); + + $this->g->removeEdge($this->v['a'], $this->v['b']); + $this->doCheckVertexCount(2); + + $this->assertTrue($this->g->hasVertex($this->v['a'])); + $this->assertTrue($this->g->hasVertex($this->v['b'])); + } + + public function testEachAdjacent() { + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->g->addDirectedEdge($this->v['a'], $this->v['c']); + + $found = array(); + $this->g->eachAdjacent($this->v['a'], function($to) use (&$found) { + $found[] = $to; + }); + + $this->assertEquals(array($this->v['b'], $this->v['c']), $found); + } + + public function testEachEdge() { + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->g->addDirectedEdge($this->v['a'], $this->v['c']); + + $found = array(); + $this->g->eachEdge(function($edge) use (&$found) { + $found[] = $edge; + }); + + $this->assertCount(2, $found); + $this->assertEquals(array($this->v['a'], $this->v['b']), $found[0]); + $this->assertEquals(array($this->v['a'], $this->v['c']), $found[1]); + } + + public function testTranspose() { + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->g->addDirectedEdge($this->v['a'], $this->v['c']); + + $transpose = $this->g->transpose(); + + $this->doCheckVertexCount(3, $transpose); + $this->doCheckVerticesEqual(array($this->v['b'], $this->v['a'], $this->v['c']), $transpose); + } +} diff --git a/core/vendor/sdboyer/gliph/tests/Gliph/Graph/UndirectedAdjacencyGraphTest.php b/core/vendor/sdboyer/gliph/tests/Gliph/Graph/UndirectedAdjacencyGraphTest.php new file mode 100644 index 0000000..7aacca4 --- /dev/null +++ b/core/vendor/sdboyer/gliph/tests/Gliph/Graph/UndirectedAdjacencyGraphTest.php @@ -0,0 +1,70 @@ +g = new UndirectedAdjacencyGraph(); + } + + public function testAddUndirectedEdge() { + $this->g->addEdge($this->v['a'], $this->v['b']); + + $this->doCheckVerticesEqual(array($this->v['a'], $this->v['b'])); + } + + public function testRemoveVertex() { + $this->g->addEdge($this->v['a'], $this->v['b']); + + $this->g->removeVertex(($this->v['a'])); + $this->doCheckVertexCount(1); + } + + public function testRemoveEdge() { + $this->g->addEdge($this->v['a'], $this->v['b']); + $this->g->addEdge($this->v['b'], $this->v['c']); + + $this->g->removeEdge($this->v['b'], $this->v['c']); + $this->doCheckVertexCount(3); + + $found = array(); + $this->g->eachAdjacent($this->v['a'], function($adjacent) use (&$found) { + $found[] = $adjacent; + }); + + $this->assertEquals(array($this->v['b']), $found); + } + + public function testEachEdge() { + $this->g->addEdge($this->v['a'], $this->v['b']); + $this->g->addEdge($this->v['b'], $this->v['c']); + + $found = array(); + $this->g->eachEdge(function ($edge) use (&$found) { + $found[] = $edge; + }); + + $this->assertCount(2, $found); + $this->assertEquals(array($this->v['a'], $this->v['b']), $found[0]); + $this->assertEquals(array($this->v['b'], $this->v['c']), $found[1]); + + // Ensure bidirectionality of created edges + $found = array(); + $this->g->eachAdjacent($this->v['b'], function($adjacent) use (&$found) { + $found[] = $adjacent; + }); + + $this->assertCount(2, $found); + } +} diff --git a/core/vendor/sdboyer/gliph/tests/Gliph/TestVertex.php b/core/vendor/sdboyer/gliph/tests/Gliph/TestVertex.php new file mode 100644 index 0000000..7a8f484 --- /dev/null +++ b/core/vendor/sdboyer/gliph/tests/Gliph/TestVertex.php @@ -0,0 +1,19 @@ +name = $name; + } + + public function __toString() { + return $this->name; + } +} \ No newline at end of file diff --git a/core/vendor/sdboyer/gliph/tests/Gliph/Traversal/DepthFirstTest.php b/core/vendor/sdboyer/gliph/tests/Gliph/Traversal/DepthFirstTest.php new file mode 100644 index 0000000..47c384d --- /dev/null +++ b/core/vendor/sdboyer/gliph/tests/Gliph/Traversal/DepthFirstTest.php @@ -0,0 +1,88 @@ +g = new DirectedAdjacencyGraph(); + $this->v = array( + 'a' => new TestVertex('a'), + 'b' => new TestVertex('b'), + 'c' => new TestVertex('c'), + 'd' => new TestVertex('d'), + 'e' => new TestVertex('e'), + 'f' => new TestVertex('f'), + 'g' => new TestVertex('g'), + ); + + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->g->addDirectedEdge($this->v['b'], $this->v['c']); + $this->g->addDirectedEdge($this->v['a'], $this->v['c']); + $this->g->addDirectedEdge($this->v['b'], $this->v['d']); + } + + public function testBasicAcyclicDepthFirstTraversal() { + $visitor = $this->getMock('Gliph\\Visitor\\DepthFirstNoOpVisitor'); + $visitor->expects($this->exactly(4))->method('onInitializeVertex'); + $visitor->expects($this->exactly(0))->method('onBackEdge'); + $visitor->expects($this->exactly(4))->method('onStartVertex'); + $visitor->expects($this->exactly(4))->method('onExamineEdge'); + $visitor->expects($this->exactly(4))->method('onFinishVertex'); + + DepthFirst::traverse($this->g, $visitor); + } + + public function testDirectCycleDepthFirstTraversal() { + $this->g->addDirectedEdge($this->v['d'], $this->v['b']); + + $visitor = $this->getMock('Gliph\\Visitor\\DepthFirstNoOpVisitor'); + $visitor->expects($this->exactly(1))->method('onBackEdge'); + + DepthFirst::traverse($this->g, $visitor); + } + + public function testIndirectCycleDepthFirstTraversal() { + $this->g->addDirectedEdge($this->v['d'], $this->v['a']); + + $visitor = $this->getMock('Gliph\\Visitor\\DepthFirstNoOpVisitor'); + $visitor->expects($this->exactly(1))->method('onBackEdge'); + + DepthFirst::traverse($this->g, $visitor, $this->v['a']); + } + + /** + * @covers Gliph\Traversal\DepthFirst::traverse + * @expectedException RuntimeException + */ + public function testExceptionOnEmptyTraversalQueue() { + // Create a cycle that ensures there are no source vertices + $this->g->addDirectedEdge($this->v['d'], $this->v['a']); + DepthFirst::traverse($this->g, new DepthFirstNoOpVisitor()); + } + + /** + * @covers Gliph\Traversal\DepthFirst::traverse + * @expectedException UnexpectedValueException + * + * This relies on the graph class to internally throw an exception + * when in attempt is made to visit a vertex that is not in the graph. + */ + public function testProvideQueueAsStartPoint() { + $queue = new \SplQueue(); + $queue->push($this->v['a']); + $queue->push($this->v['e']); + DepthFirst::traverse($this->g, new DepthFirstNoOpVisitor(), $queue); + } +} diff --git a/core/vendor/sdboyer/gliph/tests/Gliph/Visitor/DepthFirstBasicVisitorTest.php b/core/vendor/sdboyer/gliph/tests/Gliph/Visitor/DepthFirstBasicVisitorTest.php new file mode 100644 index 0000000..00f6a1d --- /dev/null +++ b/core/vendor/sdboyer/gliph/tests/Gliph/Visitor/DepthFirstBasicVisitorTest.php @@ -0,0 +1,84 @@ +v = array( + 'a' => new TestVertex('a'), + 'b' => new TestVertex('b'), + 'c' => new TestVertex('c'), + 'd' => new TestVertex('d'), + 'e' => new TestVertex('e'), + 'f' => new TestVertex('f'), + 'g' => new TestVertex('g'), + ); + + $this->g = new DirectedAdjacencyGraph(); + $this->vis = new DepthFirstBasicVisitor(); + + $this->g->addDirectedEdge($this->v['a'], $this->v['b']); + $this->g->addDirectedEdge($this->v['b'], $this->v['c']); + $this->g->addDirectedEdge($this->v['a'], $this->v['c']); + $this->g->addDirectedEdge($this->v['b'], $this->v['d']); + } + + /** + * @covers Gliph\Visitor\DepthFirstBasicVisitor::__construct + * @covers Gliph\Visitor\DepthFirstBasicVisitor::onInitializeVertex + * @covers Gliph\Visitor\DepthFirstBasicVisitor::onStartVertex + * @covers Gliph\Visitor\DepthFirstBasicVisitor::onExamineEdge + * @covers Gliph\Visitor\DepthFirstBasicVisitor::onFinishVertex + * @covers Gliph\Visitor\DepthFirstBasicVisitor::getReachable + * @covers Gliph\Visitor\DepthFirstBasicVisitor::getTsl + */ + public function testTraversalWithStartPoint() { + DepthFirst::traverse($this->g, $this->vis, $this->v['a']); + $this->assertCount(3, $this->vis->getReachable($this->v['a'])); + $this->assertCount(2, $this->vis->getReachable($this->v['b'])); + $this->assertCount(0, $this->vis->getReachable($this->v['c'])); + $this->assertCount(0, $this->vis->getReachable($this->v['d'])); + + // Not the greatest test since we're implicitly locking in to one of + // two valid TSL solutions - but that's linked to the determinism in + // the ordering of how the graph class stores vertices, which is a + // much bigger problem than can be solved right here. So, good enough. + $this->assertEquals(array($this->v['c'], $this->v['d'], $this->v['b'], $this->v['a']), $this->vis->getTsl()); + } + + /** + * @expectedException RuntimeException + * @covers Gliph\Visitor\DepthFirstBasicVisitor::onBackEdge + * @covers Gliph\Visitor\DepthFirstBasicVisitor::onInitializeVertex + */ + public function testErrorOnCycle() { + $this->g->addDirectedEdge($this->v['d'], $this->v['b']); + DepthFirst::traverse($this->g, $this->vis); + } + + /** + * @expectedException OutOfRangeException + * @covers + */ + public function testReachableExceptionOnUnknownVertex() { + DepthFirst::traverse($this->g, $this->vis, $this->v['a']); + $this->vis->getReachable($this->v['e']); + } +} diff --git a/core/vendor/sdboyer/gliph/tests/bootstrap.php b/core/vendor/sdboyer/gliph/tests/bootstrap.php new file mode 100644 index 0000000..1324983 --- /dev/null +++ b/core/vendor/sdboyer/gliph/tests/bootstrap.php @@ -0,0 +1,4 @@ +add('Gliph\\', __DIR__); \ No newline at end of file