diff --git a/composer.lock b/composer.lock index 490ccec..bd4ab17 100644 --- a/composer.lock +++ b/composer.lock @@ -338,12 +338,12 @@ "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "604c1ddef58dfc5eda97ae83c00b8e0027b72fec" + "reference": "b1ab4a10fc9f5d6931cfb9f0030fd67c8845813f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/604c1ddef58dfc5eda97ae83c00b8e0027b72fec", - "reference": "604c1ddef58dfc5eda97ae83c00b8e0027b72fec", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/b1ab4a10fc9f5d6931cfb9f0030fd67c8845813f", + "reference": "b1ab4a10fc9f5d6931cfb9f0030fd67c8845813f", "shasum": "" }, "require": { @@ -383,7 +383,7 @@ "datetime", "time" ], - "time": "2018-02-22T10:18:22+00:00" + "time": "2018-02-23T09:57:29+00:00" }, { "name": "psr/container", @@ -487,12 +487,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "996fd519af74ddfaa7733ea358acbca5d7d43f34" + "reference": "cc0d997c797f1341bdc2065bdc0bd202f4f24c67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/996fd519af74ddfaa7733ea358acbca5d7d43f34", - "reference": "996fd519af74ddfaa7733ea358acbca5d7d43f34", + "url": "https://api.github.com/repos/symfony/console/zipball/cc0d997c797f1341bdc2065bdc0bd202f4f24c67", + "reference": "cc0d997c797f1341bdc2065bdc0bd202f4f24c67", "shasum": "" }, "require": { @@ -548,7 +548,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-02-09T14:10:47+00:00" + "time": "2018-02-22T10:48:49+00:00" }, { "name": "symfony/debug", @@ -556,12 +556,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "af2d12fa3962eee6c7d2301058df1e8cf6eb273c" + "reference": "be82be767cfaeff0f753f58b13b02664cb536718" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/af2d12fa3962eee6c7d2301058df1e8cf6eb273c", - "reference": "af2d12fa3962eee6c7d2301058df1e8cf6eb273c", + "url": "https://api.github.com/repos/symfony/debug/zipball/be82be767cfaeff0f753f58b13b02664cb536718", + "reference": "be82be767cfaeff0f753f58b13b02664cb536718", "shasum": "" }, "require": { @@ -604,7 +604,7 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-02-04T13:22:04+00:00" + "time": "2018-02-22T11:40:25+00:00" }, { "name": "symfony/filesystem", @@ -612,12 +612,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e078773ad6354af38169faf31c21df0f18ace03d" + "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e078773ad6354af38169faf31c21df0f18ace03d", - "reference": "e078773ad6354af38169faf31c21df0f18ace03d", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/253a4490b528597aa14d2bf5aeded6f5e5e4a541", + "reference": "253a4490b528597aa14d2bf5aeded6f5e5e4a541", "shasum": "" }, "require": { @@ -653,7 +653,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-02-22T10:48:49+00:00" }, { "name": "symfony/finder", @@ -818,12 +818,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "db7a511c2bd8110a57c3b00364d724668cfa3c83" + "reference": "6664b21dc874109ab6c6d30af4116360d84d573f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/db7a511c2bd8110a57c3b00364d724668cfa3c83", - "reference": "db7a511c2bd8110a57c3b00364d724668cfa3c83", + "url": "https://api.github.com/repos/symfony/translation/zipball/6664b21dc874109ab6c6d30af4116360d84d573f", + "reference": "6664b21dc874109ab6c6d30af4116360d84d573f", "shasum": "" }, "require": { @@ -878,7 +878,7 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-02-19T12:10:10+00:00" + "time": "2018-02-22T11:40:25+00:00" }, { "name": "symfony/yaml", @@ -1609,12 +1609,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d6767fef9bd6f1ae69d9e7f5128c00f5f23b3d1c" + "reference": "bce5026c50e463ad720c4783ed8efae70e7cb899" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d6767fef9bd6f1ae69d9e7f5128c00f5f23b3d1c", - "reference": "d6767fef9bd6f1ae69d9e7f5128c00f5f23b3d1c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bce5026c50e463ad720c4783ed8efae70e7cb899", + "reference": "bce5026c50e463ad720c4783ed8efae70e7cb899", "shasum": "" }, "require": { @@ -1681,7 +1681,7 @@ "testing", "xunit" ], - "time": "2018-02-21T14:21:08+00:00" + "time": "2018-02-23T09:18:23+00:00" }, { "name": "phpunit/phpunit-mock-objects", diff --git a/src/Entities/ContentType.php b/src/Entities/ContentType.php index d284e26..c8ef7ab 100644 --- a/src/Entities/ContentType.php +++ b/src/Entities/ContentType.php @@ -8,6 +8,10 @@ use Tapestry\Entities\Collections\FlatCollection; use Tapestry\Modules\Renderers\ContentRendererFactory; +/** + * Class ContentType. + * @deprecated use Tapestry\Modules\ContentTypes\ContentType + */ class ContentType { /** diff --git a/src/Entities/DependencyGraph/Debug.php b/src/Entities/DependencyGraph/Debug.php new file mode 100644 index 0000000..2bac0ff --- /dev/null +++ b/src/Entities/DependencyGraph/Debug.php @@ -0,0 +1,61 @@ +graph = $graph; + } + + /** + * @param string $edge + * @param array|null $arr + * @return string + * @throws \Tapestry\Exceptions\GraphException + */ + public function graphViz(string $edge, $arr = null): String + { + if (is_null($arr)) { + $arr = [ + 'digraph Tapestry {', + ' rankdir=LR;', + ' bgcolor="0 0 .91";', + ' node [shape=circle];', + ]; + } + $arr = array_merge($arr, $this->walkGraph($edge, [])); + $arr[] = '}'; + + return implode(PHP_EOL, $arr); + } + + /** + * @param string $edge + * @param array $arr + * @return array + * @throws \Tapestry\Exceptions\GraphException + */ + private function walkGraph(string $edge, array $arr): array + { + $node = $this->graph->getEdge($edge); + foreach ($node->getEdges() as $edge) { + $arr[] = sprintf(' "%s" -> "%s"', $node->getUid(), $edge->getUid()); + if (count($edge->getEdges()) > 0) { + $arr = $this->walkGraph($edge->getUid(), $arr); + } + } + + return $arr; + } +} diff --git a/src/Entities/DependencyGraph/Graph.php b/src/Entities/DependencyGraph/Graph.php new file mode 100644 index 0000000..a826487 --- /dev/null +++ b/src/Entities/DependencyGraph/Graph.php @@ -0,0 +1,72 @@ + obj node lookup table. + * + * @var Node[] + */ + private $table = []; + + /** + * Graph constructor. + * @param Node|null $root + */ + public function __construct(Node $root = null) + { + if (! is_null($root)) { + $this->setRoot($root); + } + } + + /** + * This method acts to reset the Graph before + * setting the root node. + * + * @param Node $node + */ + public function setRoot(Node $node) + { + $this->table = []; + $this->root = $node; + $this->table[$node->getUid()] = $node; + } + + /** + * @param string $uid + * @param Node $node + * @throws GraphException + */ + public function addEdge(string $uid, Node $node) + { + if (! isset($this->table[$uid])) { + throw new GraphException('The edge ['.$uid.'] is not found in graph.'); + } + $this->table[$uid]->addEdge($node); + $this->table[$node->getUid()] = $node; + } + + /** + * @param string $uid + * @return Node + * @throws GraphException + */ + public function getEdge(string $uid): Node + { + if (! isset($this->table[$uid])) { + throw new GraphException('The edge ['.$uid.'] is not found in graph.'); + } + + return $this->table[$uid]; + } +} diff --git a/src/Entities/DependencyGraph/Node.php b/src/Entities/DependencyGraph/Node.php new file mode 100644 index 0000000..2dfb430 --- /dev/null +++ b/src/Entities/DependencyGraph/Node.php @@ -0,0 +1,41 @@ +resolved = []; + $this->unresolved = []; + $this->adjacencyList = []; + $this->resolveNode($node); + + return $this->resolved; + } + + /** + * Returns the resolved graph adjacency list. + * + * @return array + */ + public function getAdjacencyList(): array + { + return $this->adjacencyList; + } + + /** + * Reduces the resolved graph and returns only nodes that have their + * changed flag set to true or are connected as dependants to + * a node that has its changed flag set to true. + * + * @param Closure $closure + * @return array + */ + public function reduce(Closure $closure): array + { + $modified = []; + + foreach ($this->resolved as $node) { // @todo have this use a passed closure to do the evaluation + if ($closure($node) === true) { + array_push($modified, $node); + foreach ($this->adjacencyList[$node->getUid()] as $affected) { + array_push($modified, $affected); + } + } + } + + return $modified; + } + + /** + * @param Node $node + * @param Node[] $parents + * @throws \Exception + */ + private function resolveNode(Node $node, $parents = []) + { + if (! isset($this->adjacencyList[$node->getUid()])) { + $this->adjacencyList[$node->getUid()] = []; + } + + array_push($this->unresolved, $node); + foreach ($node->getEdges() as $edge) { + if (! in_array($edge, $this->resolved)) { + if (in_array($edge, $this->unresolved)) { + throw new \Exception('Circular reference detected: '.$node->getUid().' -> '.$edge->getUid()); + } + array_push($parents, $node); + $this->resolveNode($edge, $parents); + } + } + foreach ($parents as $p) { + if ($node->getUid() !== $p->getUid() && ! in_array($node, $this->adjacencyList[$p->getUid()])) { + array_push($this->adjacencyList[$p->getUid()], $node); + } + } + array_push($this->resolved, $node); + if (($key = array_search($node, $this->unresolved)) !== false) { + unset($this->unresolved[$key]); + } + } +} diff --git a/src/Entities/DependencyGraph/SimpleNode.php b/src/Entities/DependencyGraph/SimpleNode.php new file mode 100644 index 0000000..252dfe7 --- /dev/null +++ b/src/Entities/DependencyGraph/SimpleNode.php @@ -0,0 +1,103 @@ +uid = $uid; + $this->hash = $hash; + } + + /** + * Get this nodes uid. + * + * @return string + */ + public function getUid(): string + { + return $this->uid; + } + + /** + * Add a source that depends upon this source. + * + * @param Node $node + */ + public function addEdge(Node $node) + { + $this->edges[$node->getUid()] = $node; + } + + /** + * Return a list of source objects that depend upon this one. + * + * @return array + */ + public function getEdges(): array + { + return $this->edges; + } + + /** + * @return string + */ + public function getHash(): string + { + return $this->hash; + } + + /** + * @param SimpleNode|Node $node + * @return bool + * @throws GraphException + */ + public function isSame(Node $node): bool + { + if ($node->getUid() !== $this->getUid()) { + throw new GraphException('Node being compared must have the same identifier.'); + } + + if ($node->getHash() !== $this->getHash()) { + return false; + } + + return true; + } +} diff --git a/src/Entities/Permalink.php b/src/Entities/Permalink.php index cfda91d..73e6ae5 100644 --- a/src/Entities/Permalink.php +++ b/src/Entities/Permalink.php @@ -2,6 +2,8 @@ namespace Tapestry\Entities; +use Tapestry\Modules\Source\SourceInterface; + class Permalink { /** @@ -40,17 +42,17 @@ public function getTemplate() /** * Returns a compiled permalink path in string form. * - * @param ProjectFile $file + * @param SourceInterface $file * @param bool $pretty * * @return mixed|string * @throws \Exception */ - public function getCompiled(ProjectFile $file, bool $pretty = true) + public function getCompiled(SourceInterface $file, bool $pretty = true) { $output = $this->template; $output = str_replace('{ext}', $file->getExtension(), $output); - $output = str_replace('{filename}', $this->sluggify($file->getBasename('.'.$file->getExtension(false))), $output); + $output = str_replace('{filename}', $this->sluggify($file->getBasename(true)), $output); $filePath = str_replace('\\', '/', $file->getRelativePath()); if (substr($filePath, 0, 1) === '/') { diff --git a/src/Entities/Project.php b/src/Entities/Project.php index 63fd418..bec62c6 100644 --- a/src/Entities/Project.php +++ b/src/Entities/Project.php @@ -3,6 +3,9 @@ namespace Tapestry\Entities; use Tapestry\ArrayContainer; +use Tapestry\Exceptions\GraphException; +use Tapestry\Entities\DependencyGraph\Graph; +use Tapestry\Modules\Source\SourceInterface; use Tapestry\Entities\Generators\FileGenerator; use Tapestry\Entities\Collections\FlatCollection; @@ -35,7 +38,7 @@ class Project extends ArrayContainer * @param string $dist * @param string $environment */ - public function __construct($cwd, $dist, $environment) + public function __construct(string $cwd, string $dist, string $environment) { $this->sourceDirectory = $cwd.DIRECTORY_SEPARATOR.'source'; $this->destinationDirectory = $dist; @@ -46,14 +49,15 @@ public function __construct($cwd, $dist, $environment) parent::__construct( [ 'files' => new FlatCollection(), + 'graph' => new Graph(), ] ); } /** - * @param ProjectFileInterface|ProjectFile|FileGenerator $file + * @param SourceInterface|FileGenerator $file */ - public function addFile(ProjectFileInterface $file) + public function addFile(SourceInterface $file) { $this->set('files.'.$file->getUid(), $file); } @@ -61,7 +65,7 @@ public function addFile(ProjectFileInterface $file) /** * @param string $key * - * @return ProjectFileInterface|ProjectFile|FileGenerator + * @return SourceInterface */ public function getFile($key) { @@ -69,18 +73,26 @@ public function getFile($key) } /** - * @param ProjectFileInterface|ProjectFile|FileGenerator $file + * @return SourceInterface[]|FlatCollection */ - public function removeFile(ProjectFileInterface $file) + public function allSources(): FlatCollection + { + return $this->get('files'); + } + + /** + * @param SourceInterface|FileGenerator $file + */ + public function removeFile(SourceInterface $file) { $this->remove('files.'.$file->getUid()); } /** - * @param ProjectFileInterface|ProjectFile|FileGenerator $oldFile - * @param ProjectFileInterface|ProjectFile|FileGenerator $newFile + * @param SourceInterface|FileGenerator $oldFile + * @param SourceInterface|FileGenerator $newFile */ - public function replaceFile(ProjectFileInterface $oldFile, ProjectFileInterface $newFile) + public function replaceFile(SourceInterface $oldFile, SourceInterface $newFile) { $this->removeFile($oldFile); $this->addFile($newFile); @@ -88,11 +100,11 @@ public function replaceFile(ProjectFileInterface $oldFile, ProjectFileInterface /** * @param string $name - * @param ProjectFile $file + * @param SourceInterface $file * * @return ProjectFileGeneratorInterface */ - public function getContentGenerator($name, ProjectFile $file) + public function getContentGenerator($name, SourceInterface $file) { return $this->get('content_generators')->get($name, $file); } @@ -106,4 +118,17 @@ public function getContentType($name) { return $this->get('content_types.'.$name); } + + /** + * @return Graph + * @throws GraphException + */ + public function getGraph(): Graph + { + if (! $this->has('graph')) { + throw new GraphException('Graph is not initiated'); + } + + return $this->get('graph'); + } } diff --git a/src/Entities/Taxonomy.php b/src/Entities/Taxonomy.php index 4011244..1fe7983 100644 --- a/src/Entities/Taxonomy.php +++ b/src/Entities/Taxonomy.php @@ -2,6 +2,7 @@ namespace Tapestry\Entities; +use Tapestry\Modules\Source\SourceInterface; use Tapestry\Entities\Collections\Collection; class Taxonomy @@ -42,10 +43,10 @@ public function getName() } /** - * @param ProjectFile $file + * @param SourceInterface $file * @param $classification */ - public function addFile(ProjectFile $file, $classification) + public function addFile(SourceInterface $file, $classification) { $classification = str_slug($classification); if (! $this->items->has($classification)) { diff --git a/src/Entities/Tree/Leaf.php b/src/Entities/Tree/Leaf.php new file mode 100644 index 0000000..12b5bb9 --- /dev/null +++ b/src/Entities/Tree/Leaf.php @@ -0,0 +1,102 @@ +id = $id; + $this->symbol = $symbol; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return Symbol + */ + public function getSymbol(): Symbol + { + return $this->symbol; + } + + /** + * @param Leaf $entity + * @return void + */ + public function addChild(self $entity) + { + $this->hasChildren = true; + $this->children[$entity->getId()] = $entity; + } + + /** + * @return bool + */ + public function hasChildren(): bool + { + return $this->hasChildren; + } + + /** + * @return int + */ + public function childCount(): int + { + return count($this->children); + } + + /** + * @return array|Leaf[] + */ + public function getChildren(): array + { + return $this->children; + } + + /** + * @param string $id + * @return Leaf + */ + public function getChild(string $id): Leaf + { + return $this->children[$id]; + } +} diff --git a/src/Entities/Tree/Symbol.php b/src/Entities/Tree/Symbol.php new file mode 100644 index 0000000..2165c2e --- /dev/null +++ b/src/Entities/Tree/Symbol.php @@ -0,0 +1,97 @@ +id = $id; + $this->type = $type; + $this->mTime = $mTime; + $this->hash = $hash; + } + + /** + * @param string $hash + */ + public function setHash(string $hash) + { + $this->hash = $hash; + } + + /** + * Compare a Symbol (from cache) to see if this symbol is valid. + * + * Useful for reducing the tree of Symbols to just those that have + * been modified. + * + * Will return false if the symbol being compared is newer or different. + * + * Must be used against symbols of the same id, will throw an exception + * if the id is different. + * + * @param Symbol $symbol + * @return bool + * @throws \Exception + */ + public function isSame(self $symbol): bool + { + if ($symbol->id !== $this->id) { + throw new \Exception('Symbol being compared must have the same identifier.'); + } + + if ($symbol->hash !== $this->hash) { + return false; + } + + if ($this->mTime > 0 && $symbol->mTime > $this->mTime) { + return false; + } + + return true; + } +} diff --git a/src/Entities/Tree/Tree.php b/src/Entities/Tree/Tree.php new file mode 100644 index 0000000..5e205a2 --- /dev/null +++ b/src/Entities/Tree/Tree.php @@ -0,0 +1,166 @@ + + * + * @see https://github.com/jamiebuilds/itsy-bitsy-data-structures + */ +class Tree +{ + /** + * @var null|Leaf + */ + private $root = null; + + /** + * This table keeps track of which symbols are at which path. + * + * @var array + */ + private $symbolPathsHash = []; + + /** + * Traverse all items within the tree and execute the callback for + * each node in the tree. The $node param is only required for the + * recursive nature of the function, to use this method simply pass + * a valid callable. + * + * @param callable $callback + * @param null|array|Leaf $node + * @param null|array|Leaf $parent + * @param int $depth + * @return void + */ + public function traverse(callable $callback, $node = null, $parent = null, int $depth = 0) + { + if (is_null($node)) { + $this->traverse($callback, $this->root); + + return; + } + + $callback($node, $parent, $depth); + + foreach ($node->getChildren() as $child) { + $this->traverse($callback, $child, $node, ($depth + 1)); + } + } + + /** + * Helper method for traversing the tree and counting all the Leaf nodes. + * + * @return int + */ + public function childCount(): int + { + $count = 0; + $this->traverse(function () use (&$count) { + $count++; + }); + + return $count; + } + + /** + * Return all symbols stored in this Tree as a List. + * + * @return array|Symbol[] + */ + public function getAllSymbols(): array + { + $symbols = []; + $this->traverse(function (Leaf $leaf) use (&$symbols) { + if (! isset($symbols[$leaf->getId()])) { + $symbols[$leaf->getId()] = $leaf->getSymbol(); + } + }); + + return $symbols; + } + + /** + * Add a new Leaf to the tree. + * + * @deprecated replace this with addSymbol (then rename addSymbol to add) + * @param Leaf $leaf + * @param string|null $parent + * @return bool + */ + public function add(Leaf $leaf, $parent = null): bool + { + if (is_null($this->root)) { + $this->root = $leaf; + + return true; + } + + if (! is_null($parent)) { + $inserted = false; + $this->traverse(function (Leaf $node) use ($parent, $leaf, &$inserted) { + if ($node->getId() === $parent) { + $node->addChild($leaf); + $inserted = true; + } + }); + + return $inserted; + } + + return false; + } + + /** + * Add a new Symbol to the tree. + * Unlike the `add` method this attaches to parent Leaf nodes based upon + * their symbol id and not the Leaf node id. + * + * @param Symbol $symbol + * @param string|null $parent + * @return bool + */ + public function addSymbol(Symbol $symbol, $parent = null): bool + { + if (! isset($this->symbolPathsHash[$symbol->id])) { + $this->symbolPathsHash[$symbol->id] = []; + } + + if (is_null($this->root)) { + $this->root = new Leaf('root', $symbol); + $this->symbolPathsHash[$symbol->id][] = 'root'; + + return true; + } + + if (! is_null($parent)) { + $inserted = false; + $this->traverse(function (Leaf $node) use ($parent, $symbol, &$inserted) { + if ($node->getSymbol()->id === $parent) { + $node->addChild(new Leaf($node->getId().'.'.$symbol->id, $symbol)); + $this->symbolPathsHash[$symbol->id][] = $node->getId().'.'.$symbol->id; + $inserted = true; + } + }); + + if ($inserted === true && count($this->symbolPathsHash[$symbol->id]) > 1) { + // @todo check all leaf nodes for symbol have the same child structure and amend if not. + $n = 1; + } + + return $inserted; + } + + return false; + } + + /** + * @return null|Leaf + */ + public function getRoot() + { + return $this->root; + } +} diff --git a/src/Entities/Tree/TreeShaker.php b/src/Entities/Tree/TreeShaker.php new file mode 100644 index 0000000..3a198af --- /dev/null +++ b/src/Entities/Tree/TreeShaker.php @@ -0,0 +1,64 @@ +getAllSymbols(); + $changed = []; + + $a->traverse(function (Leaf $leaf) use ($symbols, &$changed) { + if (isset($symbols[$leaf->getId()])) { + if (! $leaf->getSymbol()->isSame($symbols[$leaf->getId()])) { + $changed[$leaf->getId()] = $leaf->getSymbol(); + if ($leaf->hasChildren()) { + $changed = array_merge($changed, $this->traverse($leaf)); + } + } + } + }); + + return $changed; + } + + /** + * Recursive lookup of Tree Leaves to be added to the $changed array. + * + * @param Leaf $leaf + * @param array $changed + * @return array + */ + private function traverse(Leaf $leaf, array $changed = []): array + { + foreach ($leaf->getChildren() as $child) { + $changed[$child->getId()] = $child->getSymbol(); + if ($child->hasChildren()) { + $changed = $this->traverse($child, $changed); + } + } + + return $changed; + } +} diff --git a/src/Entities/Tree/TreeToASCII.php b/src/Entities/Tree/TreeToASCII.php new file mode 100644 index 0000000..4a6e6ea --- /dev/null +++ b/src/Entities/Tree/TreeToASCII.php @@ -0,0 +1,49 @@ +tree = $tree; + } + + public function __toString() + { + $output = ''; + $this->tree->traverse(function (Leaf $leaf, Leaf $parent = null, $depth) use (&$output) { + $ascii = '├──'; + + if (! is_null($parent)) { + /** @var Leaf $last */ + $c = $parent->getChildren(); + $last = end($c); + if ($last->getId() === $leaf->getId()) { + $ascii = '└──'; + } + unset($c, $last); + } else { + $ascii = '└──'; + } + + $pad = ''; + for ($i = 0; $i < $depth * 4; $i++) { + $pad .= ' '; + } + + $output .= $pad.$ascii.$leaf->getSymbol()->id.PHP_EOL; + }); + + return $output; + } +} diff --git a/src/Exceptions/GraphException.php b/src/Exceptions/GraphException.php new file mode 100644 index 0000000..024c610 --- /dev/null +++ b/src/Exceptions/GraphException.php @@ -0,0 +1,7 @@ +get('content_types'); /** diff --git a/src/Modules/Collectors/AbstractCollector.php b/src/Modules/Collectors/AbstractCollector.php new file mode 100644 index 0000000..c937c0c --- /dev/null +++ b/src/Modules/Collectors/AbstractCollector.php @@ -0,0 +1,83 @@ +name = $name; + $this->mutatorCollection = $mutatorCollection; + $this->filterCollection = $filterCollection; + } + + /** + * @return string + */ + public function getName(): String + { + return $this->name; + } + + /** + * Iterate over this collectors mutator collection and allow each to + * mutate the SourceInterface. + * + * @param SourceInterface $source + * @return SourceInterface + */ + protected function mutateSource(SourceInterface $source) : SourceInterface + { + // @todo implement defaultData as mutators that this iterates over + foreach ($this->mutatorCollection as $mutator) { + $mutator->mutate($source); + } + + return $source; + } + + /** + * @param array|SourceInterface[] $collection + * @return array|SourceInterface[] + */ + protected function filterCollection(array $collection) + { + return array_filter($collection, function (SourceInterface $el) { + foreach ($this->filterCollection as $filter) { + if ($filter->filter($el) === true) { + return false; + } + } + + return true; + }); + } +} diff --git a/src/Modules/Collectors/CollectorCollection.php b/src/Modules/Collectors/CollectorCollection.php new file mode 100644 index 0000000..5f88265 --- /dev/null +++ b/src/Modules/Collectors/CollectorCollection.php @@ -0,0 +1,44 @@ +collectors[] = $class; + } + + /** + * Runs all collectors in collection and merges their output into one array. + * Because all file id's must be unique it will throw an Exception if there + * is a clash. + * + * @return array|SourceInterface[] + * @throws \Exception + */ + public function collect(): array + { + $output = []; + foreach ($this->collectors as $collector) { + foreach ($collector->collect() as $key => $source) { + if (isset($output[$key])) { + throw new \Exception('File with key ['.$key.'] already collected by previous collector.'); + } + $output[$key] = $source; + } + } + + return $output; + } +} diff --git a/src/Modules/Collectors/CollectorInterface.php b/src/Modules/Collectors/CollectorInterface.php new file mode 100644 index 0000000..07630d8 --- /dev/null +++ b/src/Modules/Collectors/CollectorInterface.php @@ -0,0 +1,13 @@ +ignorePaths[] = new PathExclusion($ignorePath); + } + } + + /** + * @param SourceInterface $source + * @return bool + */ + public function filter(SourceInterface $source): bool + { + foreach ($this->ignorePaths as $item) { + if ($item->filter($source) === true) { + return true; + } + } + + return false; + } +} diff --git a/src/Modules/Collectors/Exclusions/ConfigurationIgnoredExclusion.php b/src/Modules/Collectors/Exclusions/ConfigurationIgnoredExclusion.php new file mode 100644 index 0000000..f7ca278 --- /dev/null +++ b/src/Modules/Collectors/Exclusions/ConfigurationIgnoredExclusion.php @@ -0,0 +1,23 @@ +get('ignore', [])); + } +} diff --git a/src/Modules/Collectors/Exclusions/DraftsExclusion.php b/src/Modules/Collectors/Exclusions/DraftsExclusion.php new file mode 100644 index 0000000..9d91e46 --- /dev/null +++ b/src/Modules/Collectors/Exclusions/DraftsExclusion.php @@ -0,0 +1,45 @@ +canPublishDrafts = $canPublishDrafts; + } + + /** + * Returns whether the input SourceInterface should be excluded from the + * Collectors output: true = exclude, false = include. + * + * @param SourceInterface $source + * @return bool + */ + public function filter(SourceInterface $source): bool + { + if ($this->canPublishDrafts) { + return false; + } + + return $source->getData('draft', false); + } +} diff --git a/src/Modules/Collectors/Exclusions/ExclusionInterface.php b/src/Modules/Collectors/Exclusions/ExclusionInterface.php new file mode 100644 index 0000000..9cac9b3 --- /dev/null +++ b/src/Modules/Collectors/Exclusions/ExclusionInterface.php @@ -0,0 +1,20 @@ +path = $path; + } + + /** + * Returns whether the input SourceInterface should be excluded from the + * Collectors output. + * + * @param SourceInterface $source + * @return bool + */ + public function filter(SourceInterface $source): bool + { + if (strpos($source->getRelativePath(), $this->path) === false) { + return false; + } + + return true; + } +} diff --git a/src/Modules/Collectors/FilesystemCollector.php b/src/Modules/Collectors/FilesystemCollector.php new file mode 100644 index 0000000..bc39547 --- /dev/null +++ b/src/Modules/Collectors/FilesystemCollector.php @@ -0,0 +1,69 @@ +sourcePath = $sourcePath; + + parent::__construct('FilesystemCollector', $mutatorCollection, $filterCollection); + } + + /** + * Traverses source folder and returns an array containing + * all source files as instances of SplFileSource. + * + * @return array|SourceInterface[]|SplFileSource[] + * @throws \Exception + */ + public function collect(): array + { + $collection = []; + + $finder = new Finder(); + $finder->files() + ->followLinks() + ->in($this->sourcePath) + ->ignoreDotFiles(true); + + /** @var SplFileInfo $file */ + foreach ($finder->files() as $file) { + $file = new SplFileSource($file, [ + 'draft' => false, + 'date' => DateTime::createFromFormat('U', $file->getMTime()), + 'pretty_permalink' => true, + ]); + $collection[$file->getUid()] = $this->mutateSource($file); + } + + return $this->filterCollection($collection); + } +} diff --git a/src/Modules/Collectors/MemoryCollector.php b/src/Modules/Collectors/MemoryCollector.php new file mode 100644 index 0000000..390fdb1 --- /dev/null +++ b/src/Modules/Collectors/MemoryCollector.php @@ -0,0 +1,59 @@ +items = $items; + } + + /** + * @return array|SourceInterface[]|MemorySource[] + * @throws \Exception + */ + public function collect(): array + { + $collection = []; + + foreach ($this->items as $item) { + $file = new MemorySource( + $item['uid'], + $item['rawContent'], + $item['filename'], + $item['ext'], + $item['relativePath'], + $item['relativePathname'], + $item['data'] ?? [ + 'draft' => false, + 'date' => DateTime::createFromFormat('U', time()), + 'pretty_permalink' => true, + ] + ); + + $collection[$file->getUid()] = $file; + } + + return $this->filterCollection($collection); + } +} diff --git a/src/Modules/Collectors/Mutators/FrontMatterMutator.php b/src/Modules/Collectors/Mutators/FrontMatterMutator.php new file mode 100644 index 0000000..cae63bc --- /dev/null +++ b/src/Modules/Collectors/Mutators/FrontMatterMutator.php @@ -0,0 +1,20 @@ +getRawContent()); + + $source->setRenderedContent($parser->getContent()); + $source->setDataFromArray($parser->getData()); + } +} diff --git a/src/Modules/Collectors/Mutators/IsIgnoredMutator.php b/src/Modules/Collectors/Mutators/IsIgnoredMutator.php new file mode 100644 index 0000000..4412b5a --- /dev/null +++ b/src/Modules/Collectors/Mutators/IsIgnoredMutator.php @@ -0,0 +1,80 @@ +ignorePaths = $ignorePaths; + $this->exclusions = $exclusions; + } + + /** + * @param SourceInterface $source + */ + public function mutate(SourceInterface &$source) + { + $relativePath = $source->getRelativePath(); + + foreach ($this->exclusions as $exclusion) { + if (str_contains($relativePath, $exclusion)) { + $source->setIgnored(false); + + return; + } + } + + foreach ($this->ignorePaths as $ignoredPath) { + if (str_contains($relativePath, $ignoredPath)) { + $source->setIgnored(); + + return; + } + } + + // Paths containing underscores are ignored by default. + foreach (explode('/', str_replace('\\', '/', $relativePath)) as $pathItem) { + if (substr($pathItem, 0, 1) === '_') { + $source->setIgnored(); + + return; + } + } + + $source->setIgnored(false); + } +} diff --git a/src/Modules/Collectors/Mutators/IsScheduledMutator.php b/src/Modules/Collectors/Mutators/IsScheduledMutator.php new file mode 100644 index 0000000..61b1cbb --- /dev/null +++ b/src/Modules/Collectors/Mutators/IsScheduledMutator.php @@ -0,0 +1,84 @@ +now = new DateTime(); + $this->autoPublish = $autoPublish; + $this->canPublishDrafts = $canPublishDrafts; + } + + public function mutate(SourceInterface &$source) + { + // Publish Drafts / Scheduled Posts + if ($this->canPublishDrafts === false) { + // If file is a draft and cant auto publish then it remains a draft + if ( + boolval($source->getData('draft', false)) === true && + $this->canAutoPublish($source) === false + ) { + return; + } + + // While the source's front matter says its a draft its publish date is + // less than or equal to now meaning its "scheduled". + $source->setData('draft', false); + } + } + + /** + * If the file is a draft, but auto publish is enabled and the files date is in the past then it should be published. + * + * @param SourceInterface $source + * @version 1.0.9 + * @return bool + */ + private function canAutoPublish(SourceInterface $source) + { + if ($this->autoPublish === false) { + return false; + } + + if ($source->getData('date', new \DateTime()) <= $this->now) { + return true; + } + + return false; + } +} diff --git a/src/Modules/Collectors/Mutators/MutatorInterface.php b/src/Modules/Collectors/Mutators/MutatorInterface.php new file mode 100644 index 0000000..42feb5d --- /dev/null +++ b/src/Modules/Collectors/Mutators/MutatorInterface.php @@ -0,0 +1,10 @@ +getBasename(), $matches); + if (count($matches) === 3) { + $source->setDataFromArray([ + 'date' => new DateTime($matches[1]), + 'slug' => $matches[2], + 'title' => ucfirst(str_replace('-', ' ', $matches[2])), + ]); + + return; + } + + preg_match('/^(\d{2}-\d{2}-\d{4})-(.*)/', $source->getBasename(), $matches); + if (count($matches) === 3) { + $source->setDataFromArray([ + 'date' => new DateTime($matches[1]), + 'slug' => $matches[2], + 'title' => ucfirst(str_replace('-', ' ', $matches[2])), + ]); + } + } +} diff --git a/src/Modules/Collectors/PDOCollector.php b/src/Modules/Collectors/PDOCollector.php new file mode 100644 index 0000000..1c43eb3 --- /dev/null +++ b/src/Modules/Collectors/PDOCollector.php @@ -0,0 +1,46 @@ +pdo = $pdo; + } + + /** + * Executes queries on database and returns an array containing + * all source "files" as instances of MemorySource. + * + * @return array|SourceInterface[]|MemorySource[] + */ + public function collect(): array + { + // TODO: Implement collect() method. + } +} diff --git a/src/Modules/Config/DefaultConfig.php b/src/Modules/Config/DefaultConfig.php index dd11387..9bc3a34 100644 --- a/src/Modules/Config/DefaultConfig.php +++ b/src/Modules/Config/DefaultConfig.php @@ -38,6 +38,23 @@ ], ], + 'content_collectors' => [ + 'default' => [ + 'collector' => Tapestry\Modules\Collectors\FilesystemCollector::class, + 'sourcePath' => '%sourceDirectory%', + 'mutatorCollection' => [ + Tapestry\Modules\Collectors\Mutators\SetDateDataFromFileNameMutator::class, + Tapestry\Modules\Collectors\Mutators\FrontMatterMutator::class, + Tapestry\Modules\Collectors\Mutators\IsScheduledMutator::class, + Tapestry\Modules\Collectors\Mutators\IsIgnoredMutator::class, + ], + 'filterCollection' => [ + Tapestry\Modules\Collectors\Exclusions\DraftsExclusion::class, + Tapestry\Modules\Collectors\Exclusions\ConfigurationIgnoredExclusion::class, + ], + ], + ], + 'content_renderers' => [ Tapestry\Entities\Renderers\PlatesRenderer::class, Tapestry\Entities\Renderers\HTMLRenderer::class, diff --git a/src/Modules/Content/Compile.php b/src/Modules/Content/Compile.php index 0caf1ea..f31814f 100644 --- a/src/Modules/Content/Compile.php +++ b/src/Modules/Content/Compile.php @@ -18,8 +18,8 @@ use Tapestry\Entities\Generators\FileGenerator; use Tapestry\Entities\Collections\FlatCollection; use Symfony\Component\Console\Output\OutputInterface; -use Tapestry\Modules\ContentTypes\ContentTypeFactory; use Tapestry\Modules\Renderers\ContentRendererFactory; +use Tapestry\Modules\ContentTypes\ContentTypeCollection; class Compile implements Step { @@ -66,7 +66,7 @@ public function __invoke(Project $project, OutputInterface $output) { $stopwatch = $project->get('cmd_options.stopwatch', false); - /** @var ContentTypeFactory $contentTypes */ + /** @var ContentTypeCollection $contentTypes */ $contentTypes = $project->get('content_types'); /** @var ContentRendererFactory $contentRenderers */ @@ -124,11 +124,11 @@ public function __invoke(Project $project, OutputInterface $output) * Iterate over the file list of all content types and add the files they contain to the local compiled file list * also at this point run any generators that the file may be linked to. * - * @param ContentTypeFactory $contentTypes + * @param ContentTypeCollection $contentTypes * @param Project $project * @param OutputInterface $output */ - private function iterateProjectContentTypes(ContentTypeFactory $contentTypes, Project $project, OutputInterface $output) + private function iterateProjectContentTypes(ContentTypeCollection $contentTypes, Project $project, OutputInterface $output) { /** @var ContentType $contentType */ foreach ($contentTypes->all() as $contentType) { diff --git a/src/Modules/Content/LoadSourceFiles.php b/src/Modules/Content/LoadSourceFiles.php index ac6648b..a785fb6 100644 --- a/src/Modules/Content/LoadSourceFiles.php +++ b/src/Modules/Content/LoadSourceFiles.php @@ -9,8 +9,8 @@ use Symfony\Component\Finder\Finder; use Tapestry\Entities\Configuration; use Symfony\Component\Console\Output\OutputInterface; -use Tapestry\Modules\ContentTypes\ContentTypeFactory; use Tapestry\Modules\Renderers\ContentRendererFactory; +use Tapestry\Modules\ContentTypes\ContentTypeCollection; use Tapestry\Entities\Collections\ExcludedFilesCollection; class LoadSourceFiles implements Step @@ -78,7 +78,7 @@ public function __invoke(Project $project, OutputInterface $output) return false; } - /** @var ContentTypeFactory $contentTypes */ + /** @var ContentTypeCollection $contentTypes */ $contentTypes = $project->get('content_types'); foreach ($contentTypes->all() as $contentType) { diff --git a/src/Modules/ContentTypes/ContentType.php b/src/Modules/ContentTypes/ContentType.php new file mode 100644 index 0000000..83cd2c4 --- /dev/null +++ b/src/Modules/ContentTypes/ContentType.php @@ -0,0 +1,290 @@ +name = $name; + + $this->path = ((isset($settings['path']) && is_string($settings['path'])) ? $settings['path'] : ('_'.$this->name)); + $this->template = ((isset($settings['template']) && is_string($settings['template'])) ? $settings['template'] : '_templates'.DIRECTORY_SEPARATOR.$this->name); + $this->permalink = ((isset($settings['permalink']) && is_string($settings['permalink'])) ? $settings['permalink'] : ($this->name.'/{slug}.{ext}')); + $this->enabled = ((isset($settings['enabled']) && is_bool($settings['enabled'])) ? boolval($settings['enabled']) : false); + + // @todo for #31 look into this + if (isset($settings['taxonomies'])) { + foreach ($settings['taxonomies'] as $taxonomy) { + $this->taxonomies[$taxonomy] = new Taxonomy($taxonomy); + } + } + } + + /** + * Returns the name assigned to this content type on __construct. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns the path assigned to this content type on __construct. + * + * @return string + */ + public function getPath(): string + { + return $this->path; + } + + /** + * Returns the template assigned to this content type on __construct. + * + * @return string + */ + public function getTemplate(): string + { + return $this->template; + } + + public function getPermalink(): string + { + return $this->permalink; + } + + /** + * Returns whether this content type is enabled. + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * Disable/Enable this content type. + * + * @param bool $value + */ + public function setEnabled(bool $value = true) + { + $this->enabled = $value; + } + + /** + * Retrieve a Taxonomy by name. + * + * @param string $name + * @return mixed|Taxonomy + */ + public function getTaxonomy(string $name): Taxonomy + { + return $this->taxonomies[$name]; + } + + /** + * Returns all Taxonomy configured for this content type. + * + * @return array|Taxonomy[] + */ + public function getTaxonomies(): array + { + return $this->taxonomies; + } + + /** + * Assign SourceInterface to this content type. + * + * @param SourceInterface $source + * @throws \Exception + */ + public function addSource(SourceInterface $source) + { + $source->setData('contentType', $this->name); + $this->itemsOrderCache = null; + + $this->items[$source->getUid()] = $source->getData('date')->getTimestamp(); + + foreach ($this->taxonomies as $taxonomy) { + if ($classifications = $source->getData($taxonomy->getName())) { + foreach ($classifications as $classification) { + $taxonomy->addFile($source, $classification); + } + } else { + $source->setData($taxonomy->getName(), []); + } + } + } + + /** + * Returns true if SourceInterface has been assigned to this content type. + * + * @param SourceInterface $source + * @return bool + */ + public function hasSource(SourceInterface $source): bool + { + return isset($this->items[$source->getUid()]); + } + + /** + * Returns an ordered list of the source uid's that have been bucketed into + * this content type. The list is ordered by the files date. + * + * @param string $order + * @throws \Exception + * @return SourceInterface[] + */ + public function getSourceList(string $order = 'desc') + { + if (! in_array(strtolower($order), ['asc', 'desc'])) { + throw new \Exception('The order attribute of getSourceList must be either asc or desc'); + } + + if (! is_null($this->itemsOrderCache) && isset($this->itemsOrderCache[$order])) { + return $this->itemsOrderCache[$order]; + } + + // Order Files by date newer to older + uasort($this->items, function ($a, $b) use ($order) { + if ($a == $b) { + return 0; + } + if ($order === 'asc') { + return ($a < $b) ? -1 : 1; + } + + return ($a > $b) ? -1 : 1; + }); + + $this->itemsOrderCache[$order] = $this->items; + + return $this->itemsOrderCache[$order]; + } + + /** + * @param Project $project + * @throws \Exception + */ + public function mutateProjectSources(Project $project) + { + // If this content type has a template associated with it then it should set that as the + // template front matter for each file within it - unless already set. + // + // This way when we come to rendering the files md -> phtml -> html the correct template + // will be used. + + // @todo finish + + // Identify the template source file for this content type so it can be used for adding + // to the AST as well as assigning to Source files that do not already define their own + // template. + $templatePath = $project->sourceDirectory.DIRECTORY_SEPARATOR.$this->template.'.phtml'; + if ($this->name !== 'default' && file_exists($templatePath)) { + $templateSource = $project->getFile($this->templateProjectUid()); + } else { + $templateSource = null; + } + + foreach (array_keys($this->getSourceList()) as $fileKey) { + if (! $source = $project->getFile($fileKey)) { + continue; + } + + $source->setData('content_type', $this->name); + + if ($this->permalink !== '*') { + $source->setData('permalink', $this->permalink); + } + + if (! is_null($templateSource) && ! $source->hasData('template')) { + $source->setData('template', $this->template); + } + } + } + + /** + * Identify the Source UID for this ContentType's template. + * + * @return string + */ + private function templateProjectUid(): string + { + $uid = str_replace('.', '_', $this->template.'.phtml'); + $uid = str_replace(['/', '\\'], '_', $uid); + + return $uid; + } +} diff --git a/src/Modules/ContentTypes/ContentTypeFactory.php b/src/Modules/ContentTypes/ContentTypeCollection.php similarity index 61% rename from src/Modules/ContentTypes/ContentTypeFactory.php rename to src/Modules/ContentTypes/ContentTypeCollection.php index 61656a8..c241296 100644 --- a/src/Modules/ContentTypes/ContentTypeFactory.php +++ b/src/Modules/ContentTypes/ContentTypeCollection.php @@ -2,9 +2,11 @@ namespace Tapestry\Modules\ContentTypes; -use Tapestry\Entities\ContentType; +use Tapestry\Entities\Project; +use Tapestry\Modules\Source\SourceInterface; +use Tapestry\Entities\DependencyGraph\SimpleNode; -class ContentTypeFactory +class ContentTypeCollection { /** * Registered item stack. @@ -27,13 +29,22 @@ class ContentTypeFactory */ private $nameLookupTable = []; + /** + * @var Project + */ + private $project; + /** * ContentTypeFactory constructor. * * @param array|ContentType[] $items + * @param Project $project + * @throws \Exception */ - public function __construct(array $items = []) + public function __construct(array $items = [], Project $project) { + $this->project = $project; + foreach ($items as $item) { $this->add($item); } @@ -43,11 +54,11 @@ public function __construct(array $items = []) * Add a ContentType to the registry. * * @param ContentType $contentType - * @param bool $overWrite should adding overwrite existing; if false an exception will be thrown if a matching collection already found + * @param bool $overWrite should adding overwrite existing; if false an exception will be thrown if a matching collection already found * * @throws \Exception */ - public function add(ContentType $contentType, $overWrite = false) + public function add(ContentType $contentType, bool $overWrite = false) { if (! $overWrite && $this->has($contentType->getPath())) { throw new \Exception('The collection ['.$this->pathLookupTable[$contentType->getPath()].'] already collects for the path ['.$contentType->getPath().']'); @@ -56,6 +67,39 @@ public function add(ContentType $contentType, $overWrite = false) $this->items[$uid] = $contentType; $this->pathLookupTable[$contentType->getPath()] = $uid; $this->nameLookupTable[$contentType->getName()] = $uid; + + $templateFilePath = $this->project->sourceDirectory.DIRECTORY_SEPARATOR.$contentType->getTemplate().'.phtml'; + + // I have added the hash of the content types template file to ensure that the + // content type is invalid if its template changes. + if ($contentType->getName() !== 'default' && file_exists($templateFilePath)) { + $hash = sha1($uid.'.'.sha1_file($templateFilePath)); + } else { + $hash = $uid; + } + + $this->project->getGraph()->addEdge('configuration', new SimpleNode('content_type.'.$contentType->getName(), $hash)); + } + + /** + * Bucket a SourceFile into one of the ContentTypes in this Collection. + * + * @param SourceInterface $source + * @return ContentType + * @throws \Exception + */ + public function bucketSource(SourceInterface $source): ContentType + { + if (! $contentType = $this->find($source->getRelativePath())) { + $contentType = $this->get('*'); + } else { + $contentType = $this->get($contentType); + } + + $this->project->getGraph()->addEdge('content_type.'.$contentType->getName(), new SimpleNode($source->getUid(), $source->getMTime())); + $contentType->addSource($source); + + return $contentType; } /** @@ -65,7 +109,7 @@ public function add(ContentType $contentType, $overWrite = false) * * @return bool */ - public function has($path) + public function has($path): bool { return isset($this->pathLookupTable[$path]); } @@ -75,7 +119,7 @@ public function has($path) * * @return array|\Tapestry\Entities\ContentType[] */ - public function all() + public function all(): array { return array_values($this->items); } @@ -94,6 +138,8 @@ public function find($path) return $key; } } + + return null; } /** @@ -106,7 +152,7 @@ public function find($path) * * @return ContentType */ - public function get($path) + public function get($path): ContentType { if (! $this->has($path) && ! $this->has('*')) { throw new \Exception('There is no collection that collects for the path ['.$path.']'); diff --git a/src/Modules/ContentTypes/ParseContentTypes.php b/src/Modules/ContentTypes/ParseContentTypes.php deleted file mode 100644 index d805541..0000000 --- a/src/Modules/ContentTypes/ParseContentTypes.php +++ /dev/null @@ -1,80 +0,0 @@ -all() as $file) { - if (! $uses = $file->getData('use')) { - continue; - } - - foreach ($uses as $use) { - // Is this file using the content type items, or its taxonomy? - if (strpos($use, '_') !== false) { - $useParts = explode('_', $use); - $useContentType = array_shift($useParts); - $useTaxonomy = implode('_', $useParts); - - /** @var ContentType $contentType */ - if (! $contentType = $project['content_types.'.$useContentType]) { - continue; - } - - $file->setData($use.'_items', $contentType->getTaxonomy($useTaxonomy)->getFileList()); - - // If the file doesn't have a generator set then we need to define one - if (! $file->hasData('generator')) { - // do we _need_ to add a generator here? - $file->setData('generator', ['TaxonomyIndexGenerator']); - } - } else { - /** @var ContentType $contentType */ - if (! $contentType = $project['content_types.'.$use]) { - continue; - } - $file->setData($use.'_items', $contentType->getFileList()); - } - } - - $project->replaceFile($file, new FileGenerator($file)); - } - unset($file, $uses, $use, $contentType); - - /** @var ContentType $contentType */ - foreach ($project['content_types']->all() as $contentType) { - $contentType->mutateProjectFiles($project); - } - - return true; - } -} diff --git a/src/Modules/Source/AbstractSource.php b/src/Modules/Source/AbstractSource.php new file mode 100644 index 0000000..7159f13 --- /dev/null +++ b/src/Modules/Source/AbstractSource.php @@ -0,0 +1,373 @@ +edges[$node->getUid()] = $node; + } + + /** + * Return a list of source objects that depend upon this one. + * + * @return array + */ + public function getEdges(): array + { + return $this->edges; + } + + /** + * Get this sources uid. + * + * @return string + */ + public function getUid(): string + { + return $this->meta['uid']; + } + + /** + * Set this files uid. + * + * @param string$uid + * + * @return void + */ + public function setUid(string $uid) + { + $uid = str_replace('.', '_', $uid); + $uid = str_replace(['/', '\\'], '_', $uid); + $this->meta['uid'] = $uid; + } + + /** + * Set this files data (via frontmatter or other source). + * + * @param array|string $key + * @param null|mixed $value + * @throws \Exception + * + * @return void + */ + public function setData($key, $value = null) + { + if (is_array($key) && is_null($value)) { + $this->setDataFromArray($key); + + return; + } + + if ($key === 'date' && ! $value instanceof DateTime) { + $date = new DateTime(); + if (! $unix = strtotime($value)) { + if (! $unix = strtotime('@'.$value)) { + throw new \Exception('The date ['.$value.'] is in a format not supported by Tapestry.'); + } + } + $value = $date->createFromFormat('U', $unix); + } + + if ($key === 'permalink') { + $this->permalink->setTemplate($value); + } + + $this->meta[$key] = $value; + } + + /** + * Set this files data from array source. + * + * @param array $data + * @throws \Exception + * + * @return void + */ + public function setDataFromArray(array $data) + { + foreach ($data as $key => $value) { + $this->setData($key, $value); + } + } + + /** + * Return this files data (set via frontmatter if any is found). + * + * @param null|string $key + * @param null $default + * + * @return array|mixed|null + */ + public function getData(string $key = null, $default = null) + { + if (is_null($key)) { + return $this->meta; + } + if (! $this->hasData($key)) { + return $default; + } + + return $this->meta[$key]; + } + + /** + * Return true if this file has data set for $key. + * + * @param $key + * + * @return bool + */ + public function hasData(string $key): bool + { + return isset($this->meta[$key]); + } + + /** + * A file can be considered loaded once its content property + * has been set, that way you know any front matter has also + * been injected into the File objects data property. + * + * @return bool + */ + public function hasContent(): bool + { + return $this->content !== false; + } + + /** + * Returns the file's Permalink. + * + * @return Permalink + */ + public function getPermalink(): Permalink + { + return $this->permalink; + } + + /** + * Pretty Permalinks are disabled on all files that have their + * permalink structure configured via front matter. + * + * @return mixed|string + * @throws \Exception + */ + public function getCompiledPermalink(): string + { + $pretty = $this->getData('pretty_permalink', true); + if ($this->hasData('permalink')) { + $pretty = false; + } + + return $this->permalink->getCompiled($this, $pretty); + } + + /** + * Returns the file content, this will be excluding any frontmatter. + * + * @throws \Exception + * @return string + */ + public function getRenderedContent(): string + { + if (! $this->hasContent()) { + throw new \Exception('The file ['.$this->getRelativePathname().'] has not been loaded.'); + } + + return $this->content; + } + + /** + * Get the filename. + * Without the file extension. + * + * @param bool $overloaded + * @return string + */ + public function getBasename(bool $overloaded = true): string + { + $e = explode('.', $this->getFilename($overloaded)); + array_pop($e); + + return implode('.', $e); + } + + /** + * Has the file changed since it was last processed? + * + * @return bool + */ + public function hasChanged(): bool + { + return $this->hasChanged; + } + + /** + * Set the hasChanged flag. + * + * @param bool $value + */ + public function setHasChanged(bool $value = true) + { + $this->hasChanged = $value; + } + + /** + * Has this source been through the renderer step? + * + * @return bool + */ + public function isRendered(): bool + { + return $this->rendered; + } + + /** + * Set the rendered flag. + * + * @param bool $value + */ + public function setRendered(bool $value = true) + { + $this->rendered = $value; + } + + /** + * Should the file be copied from source to dist, or processed? + * + * @return bool + */ + public function isToCopy(): bool + { + return $this->copy; + } + + /** + * Set the copy flag. + * + * @param bool $value + */ + public function setToCopy(bool $value = true) + { + $this->copy = $value; + } + + /** + * Is the source to be ignored by the compile steps? + * + * @return bool + */ + public function isIgnored(): bool + { + return $this->ignored; + } + + /** + * Set the ignore flag. + * + * @param bool $value + */ + public function setIgnored(bool $value = true) + { + $this->ignored = $value; + } + + /** + * Set the value of an overloaded property. + * + * @param string $key + * @param mixed $value + */ + public function setOverloaded(string $key, $value) + { + $this->overloaded[$key] = $value; + } + + /** + * @param Node $node + * @return bool + */ + public function isSame(Node $node): bool + { + return true; + // TODO: Implement isSame() method. + } +} diff --git a/src/Modules/Source/MemorySource.php b/src/Modules/Source/MemorySource.php new file mode 100644 index 0000000..36d6458 --- /dev/null +++ b/src/Modules/Source/MemorySource.php @@ -0,0 +1,166 @@ +meta = []; + $this->permalink = new Permalink(); + $this->setDataFromArray($data); + $this->setUid($uid); + $this->filename = $filename; + $this->ext = $ext; + $this->relativePath = $relativePath; + $this->relativePathname = $relativePathname; + $this->rawContent = $rawContent; + $this->mTime = time(); + } + + /** + * Get the content of the file that this object relates to. + * + * @throws \Exception + * @return string + */ + public function getRawContent(): string + { + return $this->rawContent; + } + + /** + * Set the files content, this should be excluding any frontmatter. + * + * @param string $content + */ + public function setRenderedContent(string $content) + { + $this->content = $content; + } + + /** + * Gets the filename. + * + * @param bool $overloaded + * @return string + */ + public function getFilename(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['filename'])) { + return $this->overloaded['filename']; + } + + return $this->filename; + } + + /** + * Gets the file extension. + * + * @param bool $overloaded + * @return string + */ + public function getExtension(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['ext'])) { + return $this->overloaded['ext']; + } + + return $this->ext; + } + + /** + * Returns the relative path. + * This path does not contain the file name. + * + * @param bool $overloaded + * @return string the relative path + */ + public function getRelativePath(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['relativePath'])) { + return $this->overloaded['relativePath']; + } + + return $this->relativePath; + } + + /** + * Returns the relative path name. + * This path contains the file name. + * + * @param bool $overloaded + * @return string + */ + public function getRelativePathname(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['relativePathname'])) { + return $this->overloaded['relativePathname']; + } + + return $this->relativePathname; + } + + /** + * Returns the last modified time. + * + * @return int + */ + public function getMTime(): int + { + return $this->mTime; + } +} diff --git a/src/Modules/Source/SourceInterface.php b/src/Modules/Source/SourceInterface.php new file mode 100644 index 0000000..27954b4 --- /dev/null +++ b/src/Modules/Source/SourceInterface.php @@ -0,0 +1,62 @@ +splFileInfo = $file; + $this->meta = []; + $this->permalink = new Permalink(); + + $this->setDataFromArray($data); + $this->setUid((! empty($this->getRelativePathname())) ? $this->getRelativePathname() : $file->getPathname()); + + // if ($autoBoot === true) { + // $this->boot($data); + // } + } + + /** + * Get the content of the file that this object relates to. + * + * @throws \Exception + * @return string + */ + public function getRawContent(): string + { + return $this->splFileInfo->getContents(); + } + + /** + * Set the files content, this should be excluding any frontmatter. + * + * @param string $content + */ + public function setRenderedContent(string $content) + { + $this->content = $content; + } + + /** + * Gets the filename. + * + * @param bool $overloaded + * @return string + */ + public function getFilename(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['filename'])) { + return $this->overloaded['filename']; + } + + return $this->splFileInfo->getFilename(); + } + + /** + * Gets the file extension. + * + * @param bool $overloaded + * @return string + */ + public function getExtension(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['ext'])) { + return $this->overloaded['ext']; + } + + return $this->splFileInfo->getExtension(); + } + + /** + * Returns the relative path. + * This path does not contain the file name. + * + * @param bool $overloaded + * @return string the relative path + */ + public function getRelativePath(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['relativePath'])) { + return $this->overloaded['relativePath']; + } + + return $this->splFileInfo->getRelativePath(); + } + + /** + * Returns the relative path name. + * This path contains the file name. + * + * @param bool $overloaded + * @return string + */ + public function getRelativePathname(bool $overloaded = true): string + { + if ($overloaded === true && isset($this->overloaded['relativePathname'])) { + return $this->overloaded['relativePathname']; + } + + return $this->splFileInfo->getRelativePathname(); + } + + /** + * Returns the last modified time. + * + * @return int + */ + public function getMTime(): int + { + return $this->splFileInfo->getMTime(); + } +} diff --git a/src/Providers/CollectorsServiceProvider.php b/src/Providers/CollectorsServiceProvider.php new file mode 100644 index 0000000..22c04a4 --- /dev/null +++ b/src/Providers/CollectorsServiceProvider.php @@ -0,0 +1,95 @@ +container property or the `getContainer` method + * from the ContainerAwareTrait. + * + * @return void + */ + public function register() + { + $this->registerIsScheduledMutatorFactory(); + $this->registerIsIgnoredMutatorFactory(); + $this->registerDraftsExclusionFactory(); + } + + private function registerDraftsExclusionFactory() + { + $container = $this->getContainer(); + $container->add(DraftsExclusion::class, function () use ($container) { + /** @var Configuration $configuration */ + $configuration = $container->get(Configuration::class); + + $publishDrafts = boolval($configuration->get('publish_drafts', false)); + + return new DraftsExclusion($publishDrafts); + }); + } + + private function registerIsIgnoredMutatorFactory() + { + $container = $this->getContainer(); + $container->add(IsIgnoredMutator::class, function () use ($container) { + /** @var Project::class $project */ + $project = $container->get(Project::class); + + /** @var Configuration $configuration */ + $configuration = $container->get(Configuration::class); + + /** @var ContentTypeCollection $contentTypes */ + $contentTypes = $project->get('content_types'); + + $exclusions = []; + foreach ($contentTypes->all() as $contentType) { + $path = $contentType->getPath(); + if ($path !== '*' && ! isset($this->dontIgnorePaths[$contentType->getPath()])) { + $exclusions[] = $contentType->getPath(); + } + } + unset($contentType); + + return new IsIgnoredMutator(array_merge($configuration->get('ignore', []), ['_views', '_templates']), $exclusions); + }); + } + + private function registerIsScheduledMutatorFactory() + { + $container = $this->getContainer(); + $container->add(IsScheduledMutator::class, function () use ($container) { + /** @var Tapestry $tapestry */ + $tapestry = $container->get(Tapestry::class); + + /** @var Configuration $configuration */ + $configuration = $container->get(Configuration::class); + + $publishDrafts = boolval($configuration->get('publish_drafts', false)); + + $autoPublish = (isset($tapestry['cmd_options']['auto-publish']) ? boolval($tapestry['cmd_options']['auto-publish']) : false); + + return new IsScheduledMutator($publishDrafts, $autoPublish); + }); + } +} diff --git a/src/Modules/Kernel/BootKernel.php b/src/Steps/BootKernel.php similarity index 79% rename from src/Modules/Kernel/BootKernel.php rename to src/Steps/BootKernel.php index f2be615..242ebc4 100644 --- a/src/Modules/Kernel/BootKernel.php +++ b/src/Steps/BootKernel.php @@ -1,13 +1,19 @@ index.phtml -> BlogPosts (paginated 3 per page) -> post-1.md - // -> post-2.md - // -> post-3.md + // For speed other Steps take part in the LexicalAnalysis and building the AST Tree. + // The role of this Step is to complete the job so that the Compilation steps can + // focus on only dealing with Tree nodes that have changed since the last run. // + + $graph = $project->getGraph(); + + foreach ($project->allSources() as $source) { + if ($source->isToCopy()) { + continue; + } + + // @todo needs to identify phtml layout's for the graph. + if ($template = $source->getData('template')) { + if (strpos($template, '.') === false) { + $template .= '.phtml'; + } + $graph->addEdge($this->templateUid($template), $graph->getEdge($source->getUid())); + } + + // Plates v4 uses $v->layout and $v->insert to define dependencies + if ($source->getExtension() === 'phtml') { + $tokens = token_get_all($source->getRenderedContent()); + foreach ($tokens as $k => $token) { + if ($token[0] === T_VARIABLE && $token[1] === '$v') { + if ($tokens[$k + 1][0] === T_OBJECT_OPERATOR) { + if ($tokens[$k + 2][0] === T_STRING && ($tokens[$k + 2][1] === 'layout' || $tokens[$k + 2][1] === 'insert')) { + if ($tokens[$k + 3] === '(' && $tokens[$k + 4][0] === T_CONSTANT_ENCAPSED_STRING) { + $found = substr($tokens[$k + 4][1], 1, -1); + if (strpos($found, '.phtml') === false) { + $found .= '.phtml'; + } + $graph->addEdge($this->templateUid($found), $graph->getEdge($source->getUid())); + } + } + } + } + } + } + } + + // @todo reduce the graph and provide the compile steps with a list of source files that are queued for compilation + + $debug = new Debug($graph); + file_put_contents(__DIR__.'/../../lexical.gv', $debug->graphViz('kernel')); // @todo remove afterwards + return true; } + + /** + * Identify the Source UID for this ContentType's template. + * + * @param string $template + * @return string + */ + private function templateUid(string $template): string + { + $uid = str_replace('.', '_', $template); + $uid = str_replace(['/', '\\'], '_', $uid); + + return $uid; + } } diff --git a/src/Steps/LoadContentCollectors.php b/src/Steps/LoadContentCollectors.php new file mode 100644 index 0000000..3002f05 --- /dev/null +++ b/src/Steps/LoadContentCollectors.php @@ -0,0 +1,102 @@ +configuration = $configuration; + $this->container = $tapestry->getContainer(); + } + + /** + * Process the Project at current. + * + * @param Project $project + * @param OutputInterface $output + * + * @return bool + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \ReflectionException + */ + public function __invoke(Project $project, OutputInterface $output): Bool + { + $collection = new CollectorCollection(); + + foreach ($this->configuration->get('content_collectors', []) as $name => $collectorConfig) { + // Replace any %xxx% values with public Project properties. + foreach ($collectorConfig as $key => $value) { + if (! is_string($value)) { + continue; + } + if (preg_match_all("/%(\w+)%/", $value, $matches) > 0) { + $collectorConfig[$key] = $project->{$matches[1][0]}; + } + } + + // If mutatorCollection exist, create their classes. + if (isset($collectorConfig['mutatorCollection'])) { + foreach ($collectorConfig['mutatorCollection'] as &$mutator) { + $mutator = $this->container->get($mutator); + } + unset($mutator); + } + + // If filterCollection exist, create their classes. + if (isset($collectorConfig['filterCollection'])) { + foreach ($collectorConfig['filterCollection'] as &$exclusion) { + $exclusion = $this->container->get($exclusion); + } + unset($exclusion); + } + + $class = new \ReflectionClass($collectorConfig['collector']); + $params = []; + + foreach ($class->getConstructor()->getParameters() as $parameter) { + $params[$parameter->name] = $collectorConfig[$parameter->name]; + } + + /** @var CollectorInterface $instance */ + $instance = $class->newInstanceArgs($params); + $collection->add($instance); + } + + $project['content_collectors'] = $collection; + + return true; + } +} diff --git a/src/Steps/LoadContentTypes.php b/src/Steps/LoadContentTypes.php index c0677eb..09c7da2 100644 --- a/src/Steps/LoadContentTypes.php +++ b/src/Steps/LoadContentTypes.php @@ -4,11 +4,16 @@ use Tapestry\Step; use Tapestry\Entities\Project; -use Tapestry\Entities\ContentType; use Tapestry\Entities\Configuration; +use Tapestry\Modules\ContentTypes\ContentType; use Symfony\Component\Console\Output\OutputInterface; -use Tapestry\Modules\ContentTypes\ContentTypeFactory; +use Tapestry\Modules\ContentTypes\ContentTypeCollection; +/** + * Class LoadContentTypes. + * + * This Step loads the configured content types into the Project Container. + */ class LoadContentTypes implements Step { /** @@ -41,13 +46,13 @@ public function __invoke(Project $project, OutputInterface $output) $output->writeln('[!] Your project\'s content types are miss-configured. Doing nothing and exiting.]'); } - $contentTypeFactory = new ContentTypeFactory([ + $contentTypeFactory = new ContentTypeCollection([ new ContentType('default', [ 'path' => '*', 'permalink' => '*', 'enabled' => true, ]), - ]); + ], $project); foreach ($contentTypes as $name => $settings) { $contentTypeFactory->add(new ContentType($name, $settings)); diff --git a/src/Steps/LoadGraph.php b/src/Steps/LoadGraph.php new file mode 100644 index 0000000..29fb29e --- /dev/null +++ b/src/Steps/LoadGraph.php @@ -0,0 +1,59 @@ +tapestry = $tapestry; + $this->configuration = $configuration; + } + + /** + * Process the Project at current. + * + * @param Project $project + * @param OutputInterface $output + * + * @return bool + * @throws \Exception + */ + public function __invoke(Project $project, OutputInterface $output) + { + $graph = $project->getGraph(); + + /** @var KernelInterface $kernel */ + $kernel = $this->tapestry->getContainer()->get(KernelInterface::class); + + $reflection = new \ReflectionClass($kernel); + $graph->setRoot(new SimpleNode('kernel', sha1_file($reflection->getFileName()))); + $graph->addEdge('kernel', new SimpleNode('configuration', sha1(json_encode($this->configuration->all())))); + + return true; + } +} diff --git a/src/Steps/LoadSourceFileTree.php b/src/Steps/LoadSourceFileTree.php index 1a4471e..78aff86 100644 --- a/src/Steps/LoadSourceFileTree.php +++ b/src/Steps/LoadSourceFileTree.php @@ -11,8 +11,8 @@ use Tapestry\Entities\Configuration; use Tapestry\Modules\Content\FrontMatter; use Symfony\Component\Console\Output\OutputInterface; -use Tapestry\Modules\ContentTypes\ContentTypeFactory; use Tapestry\Modules\Renderers\ContentRendererFactory; +use Tapestry\Modules\ContentTypes\ContentTypeCollection; class LoadSourceFileTree implements Step { @@ -97,7 +97,7 @@ public function __invoke(Project $project, OutputInterface $output) $hashTable = []; } - /** @var ContentTypeFactory $contentTypes */ + /** @var ContentTypeCollection $contentTypes */ $contentTypes = $project->get('content_types'); foreach ($contentTypes->all() as $contentType) { diff --git a/src/Steps/ParseContentTypes.php b/src/Steps/ParseContentTypes.php new file mode 100644 index 0000000..41e3168 --- /dev/null +++ b/src/Steps/ParseContentTypes.php @@ -0,0 +1,97 @@ +allSources() as $source) { + /** @var string[] $uses */ + if (! $uses = $source->getData('use')) { + continue; + } + + foreach ($uses as $use) { + if (strpos($use, '_') !== false) { + // This is a request for a taxonomy from a content type e.g `blog_categories` + $useParts = explode('_', $use); + $useContentType = array_shift($useParts); + $useTaxonomy = implode('_', $useParts); + + /** @var ContentType $contentType */ + if (! $contentType = $project['content_types.'.$useContentType]) { + continue; + } + + $source->setData($use.'_items', $contentType->getTaxonomy($useTaxonomy)->getFileList()); + + // If the file doesn't have a generator set then we need to define one + if (! $source->hasData('generator')) { + // do we _need_ to add a generator here? + $source->setData('generator', ['TaxonomyIndexGenerator']); + } + unset($useParts, $useContentType, $useTaxonomy); + } else { + // This is a request for the items in a content type e.g `blog` + /** @var ContentType $contentType */ + if (! $contentType = $project['content_types.'.$use]) { + continue; + } + + $source->setData($use.'_items', $contentType->getSourceList()); + } + } + + unset($file, $uses, $use, $contentType); + } + + /** @var ContentType $contentType */ + foreach ($project['content_types']->all() as $contentType) { + $contentType->mutateProjectSources($project); + } + + return true; + } +} diff --git a/src/Steps/ReadCache.php b/src/Steps/ReadCache.php index 1214cb3..61d7a95 100644 --- a/src/Steps/ReadCache.php +++ b/src/Steps/ReadCache.php @@ -10,6 +10,11 @@ use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Console\Output\OutputInterface; +/** + * Class ReadCache. + * + * This Step opens the cache file if found for the configured environment and loads it into the Project Container. + */ class ReadCache implements Step { /** @@ -27,7 +32,7 @@ public function __construct(Finder $finder) } /** - * Invoke a new instance of the Cache system, load it and then inject it into the Project container. + * Invoke a new instance of the Cache system, load it and then inject it into the Project Container. * * @param Project $project * @param OutputInterface $output diff --git a/src/Steps/RunContentCollectors.php b/src/Steps/RunContentCollectors.php new file mode 100644 index 0000000..222ef5a --- /dev/null +++ b/src/Steps/RunContentCollectors.php @@ -0,0 +1,38 @@ +get('content_collectors'); + + /** @var ContentTypeCollection $contentTypes */ + $contentTypes = $project->get('content_types'); + + foreach ($collection->collect() as $source) { + $contentType = $contentTypes->bucketSource($source); + $project->addFile($source); + $output->writeln('[+] File ['.$source->getRelativePathname().'] bucketed into content type ['.$contentType->getName().']'); + } + + return true; + } +} diff --git a/src/Steps/RunGenerators.php b/src/Steps/RunGenerators.php new file mode 100644 index 0000000..6781882 --- /dev/null +++ b/src/Steps/RunGenerators.php @@ -0,0 +1,34 @@ +get('content_types'); /** @var ContentRendererFactory $contentRenderers */ @@ -134,12 +134,12 @@ private function writeIntermediateFiles(Project $project, ContentRendererFactory * Iterate over the file list of all content types and add the files they contain to the local compiled file list * also at this point run any generators that the file may be linked to. * - * @param ContentTypeFactory $contentTypes + * @param ContentTypeCollection $contentTypes * @param Project $project * @param OutputInterface $output * @throws \Exception */ - private function iterateProjectContentTypes(ContentTypeFactory $contentTypes, Project $project, OutputInterface $output) + private function iterateProjectContentTypes(ContentTypeCollection $contentTypes, Project $project, OutputInterface $output) { foreach ($contentTypes->all() as $contentType) { $output->writeln('[+] Compiling content within ['.$contentType->getName().']'); diff --git a/src/Tapestry.php b/src/Tapestry.php index 3468ef1..cc4723c 100644 --- a/src/Tapestry.php +++ b/src/Tapestry.php @@ -126,6 +126,7 @@ public function boot() $this->register(\Tapestry\Providers\ProjectServiceProvider::class); $this->register(\Tapestry\Providers\PlatesServiceProvider::class); $this->register(\Tapestry\Providers\ProjectKernelServiceProvider::class); + $this->register(\Tapestry\Providers\CollectorsServiceProvider::class); } /** diff --git a/tests/Mocks/01-02-2018-this-is-a-test.md b/tests/Mocks/01-02-2018-this-is-a-test.md new file mode 100644 index 0000000..acd9ecb --- /dev/null +++ b/tests/Mocks/01-02-2018-this-is-a-test.md @@ -0,0 +1,6 @@ +--- +title: Test File Title +draft: false +--- + +This is a test file... \ No newline at end of file diff --git a/tests/Mocks/2018-02-01-this-is-a-test.md b/tests/Mocks/2018-02-01-this-is-a-test.md new file mode 100644 index 0000000..acd9ecb --- /dev/null +++ b/tests/Mocks/2018-02-01-this-is-a-test.md @@ -0,0 +1,6 @@ +--- +title: Test File Title +draft: false +--- + +This is a test file... \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index ae6db56..3f9af35 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,9 +6,12 @@ use RecursiveIteratorIterator; use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Finder\SplFileInfo; use Tapestry\Console\Application; use Tapestry\Console\DefaultInputDefinition; use Tapestry\Console\Input; +use Tapestry\Modules\Source\MemorySource; +use Tapestry\Modules\Source\SplFileSource; use Tapestry\Tapestry; class TestCase extends \PHPUnit\Framework\TestCase { @@ -173,6 +176,36 @@ protected function runCommand(string $command, string $argv = '', array $options return $applicationTester; } + /** + * @param $file + * @param $relativePath + * @param $relativePathname + * @return SplFileSource + */ + public function mockSplFileSource(string $file, string $relativePath, string $relativePathname): SplFileSource { + try { + return new SplFileSource(new SplFileInfo($file, $relativePath, $relativePathname)); + } catch (\Exception $e) { + $this->fail($e->getMessage()); + return null; + } + } + + /** + * @param string $uid + * @param string $ext + * @return MemorySource + */ + public function mockMemorySource(string $uid, string $ext = 'md'): MemorySource + { + try { + return new MemorySource($uid, '', ($uid . '.' . $ext), $ext, '', ($uid . '.' . $ext)); + } catch (\Exception $e) { + $this->fail($e->getMessage()); + return null; + } + } + /** * Asserts that the contents of one file is equal to the contents of another * file. diff --git a/tests/Unit/ContentGraphNTest.php b/tests/Unit/ContentGraphNTest.php index cbe5496..32c8afd 100644 --- a/tests/Unit/ContentGraphNTest.php +++ b/tests/Unit/ContentGraphNTest.php @@ -5,15 +5,17 @@ use Symfony\Component\Console\Output\NullOutput; use Tapestry\Entities\Project; use Tapestry\Generator; -use Tapestry\Modules\ContentTypes\ParseContentTypes; +use Tapestry\Steps\BootKernel; +use Tapestry\Steps\LoadContentCollectors; +use Tapestry\Steps\LoadGraph; +use Tapestry\Steps\ParseContentTypes; use Tapestry\Steps\LexicalAnalysis; use Tapestry\Steps\LoadContentGenerators; use Tapestry\Steps\LoadContentRenderers; use Tapestry\Steps\LoadContentTypes; -use Tapestry\Steps\LoadSourceFileTree; use Tapestry\Steps\ReadCache; -use Tapestry\Steps\RenderPlates; -use Tapestry\Steps\SyntaxAnalysis; +use Tapestry\Steps\RunContentCollectors; +use Tapestry\Steps\RunGenerators; use Tapestry\Tests\TestCase; use Tapestry\Tests\Traits\MockTapestry; @@ -28,6 +30,8 @@ class ContentGraphNTest extends TestCase */ public function testAnalysis() { + //$this->assertTrue(true); return; + //$this->loadToTmp($this->assetPath('build_test_7/src')); $this->loadToTmp($this->assetPath('build_test_41/src')); $tapestry = $this->mockTapestry($this->tmpDirectory); @@ -36,40 +40,33 @@ public function testAnalysis() $project = $tapestry->getContainer()->get(Project::class); $generator = new Generator([ + // Loading... + BootKernel::class, ReadCache::class, + LoadGraph::class, LoadContentTypes::class, + LoadContentCollectors::class, LoadContentRenderers::class, LoadContentGenerators::class, - LoadSourceFileTree::class, + // Collecting... + RunContentCollectors::class, + + // Parsing/Lexical Analysis ParseContentTypes::class, + LexicalAnalysis::class, - SyntaxAnalysis::class, - //LexicalAnalysis::class, - RenderPlates::class - ], $tapestry); - $generator->generate($project, new NullOutput()); + // Generation/Compilation... + RunGenerators::class, + //SyntaxAnalysis::class, + //RenderPlates::class - //touch($this->tmpDirectory . '/source/something.html'); + // Shutdown... - //$generator->generate($project, new NullOutput()); - // - // For refactoring issues #300, #297, #284, #282, #270: - // - // Tapestry now parses all files in the source folder and builds an hash table containing them all - // and their last change date. - // - // Test that the following happens: - // - // [ ] All files in source folder are loaded - // [ ] All files in source folder are bucketed correctly - // - // Because ignored files are still "parsed" during the LoadSourceFileTree step it also needs to be checked that - // they have an ignored flag set. This ensures that template files don't then get copied from source to dist. - // - // [ ] Ignored files are ignored - // + ], $tapestry); + + $this->assertEquals(0, $generator->generate($project, new NullOutput())); - $f = 0; + $this->assertTrue(true); } } \ No newline at end of file diff --git a/tests/Unit/ContentTypeNTest.php b/tests/Unit/ContentTypeNTest.php index facc80e..de3a219 100644 --- a/tests/Unit/ContentTypeNTest.php +++ b/tests/Unit/ContentTypeNTest.php @@ -3,9 +3,11 @@ namespace Tapestry\Tests\Unit; use Symfony\Component\Finder\SplFileInfo; -use Tapestry\Entities\ContentType; -use Tapestry\Entities\ProjectFile; -use Tapestry\Modules\ContentTypes\ContentTypeFactory; +use Tapestry\Entities\DependencyGraph\SimpleNode; +use Tapestry\Entities\Project; +use Tapestry\Modules\ContentTypes\ContentType; +use Tapestry\Modules\ContentTypes\ContentTypeCollection; +use Tapestry\Modules\Source\SplFileSource; use Tapestry\Tests\TestCase; class ContentTypeNTest extends TestCase @@ -15,12 +17,12 @@ class ContentTypeNTest extends TestCase * Added for issue 88 * @see https://github.com/carbontwelve/tapestry/issues/88 */ - public function testAddFileMutatesFileDataWithContentTypeName() + public function testAddSourceMutatesFileDataWithContentTypeName() { $contentType = new ContentType('Test', ['enabled' => true]); - $file = new ProjectFile(new SplFileInfo(__DIR__ . '/../Mocks/TestFile.md', '', '')); + $file = new SplFileSource(new SplFileInfo(__DIR__ . '/../Mocks/TestFile.md', '', ''), ['date' => new \DateTime()]); $this->assertFalse($file->hasData('contentType')); - $contentType->addFile($file); + $contentType->addSource($file); $this->assertTrue($file->hasData('contentType')); } @@ -30,10 +32,12 @@ public function testAddFileMutatesFileDataWithContentTypeName() */ public function testContentTypeFactoryArrayAccessByKey() { + $project = new Project('', '',''); + $project->getGraph()->setRoot(new SimpleNode('configuration', 'hello world')); $contentType = new ContentType('Test', ['enabled' => true]); - $contentTypeFactory = new ContentTypeFactory([ + $contentTypeFactory = new ContentTypeCollection([ $contentType - ]); + ], $project); $this->assertTrue($contentTypeFactory->has('_Test')); $this->assertEquals($contentType, $contentTypeFactory->arrayAccessByKey('Test')); $this->assertEquals(null, $contentTypeFactory->arrayAccessByKey('NonExistant')); diff --git a/tests/Unit/DependencyGraphNTest.php b/tests/Unit/DependencyGraphNTest.php new file mode 100644 index 0000000..eea96f6 --- /dev/null +++ b/tests/Unit/DependencyGraphNTest.php @@ -0,0 +1,143 @@ +addEdge($nodes['b']); // b depends on a + $nodes['a']->addEdge($nodes['d']); // d depends on a + $nodes['b']->addEdge($nodes['c']); // c depends on b + $nodes['b']->addEdge($nodes['e']); // e depends on b + $nodes['c']->addEdge($nodes['d']); // d depends on c + $nodes['c']->addEdge($nodes['e']); // e depends on c + + $class = new Resolver(); + $result = $class->resolve($nodes['a']); + + $this->assertSame(['memory_d', 'memory_e', 'memory_c', 'memory_b', 'memory_a'], array_map(function(Node $v){ + return $v->getUid(); + }, $result)); + } catch (\Exception $e) { + $this->fail($e); + return; + } + } + + public function testResolverCircularDetection() + { + try{ + /** @var Node[] $nodes */ + $nodes = []; + foreach (range('a', 'e') as $letter) { + $nodes[$letter] = new MemorySource('memory_' . $letter, 'Howdy!', 'memory.md', 'md', 'memory/' . $letter, 'memory/' . $letter . '/memory.md'); + } + } catch (\Exception $e) { + $this->fail($e); + return; + } + $nodes['a']->addEdge($nodes['b']); // b depends on a + $nodes['a']->addEdge($nodes['d']); // d depends on a + $nodes['b']->addEdge($nodes['c']); // c depends on b + $nodes['b']->addEdge($nodes['e']); // e depends on b + $nodes['c']->addEdge($nodes['d']); // d depends on c + $nodes['c']->addEdge($nodes['e']); // e depends on c + $nodes['d']->addEdge($nodes['b']); // b depends on d - circular + + $class = new Resolver(); + $this->expectExceptionMessage('Circular reference detected: memory_d -> memory_b'); + $class->resolve($nodes['a']); + } + + public function testGraphAdjacencyList() + { + try{ + /** @var Node[] $nodes */ + $nodes = []; + foreach (range('a', 'e') as $letter) { + $nodes[$letter] = new MemorySource('memory_' . $letter, 'Howdy!', 'memory.md', 'md', 'memory/' . $letter, 'memory/' . $letter . '/memory.md'); + } + + $nodes['a']->addEdge($nodes['b']); // b depends on a + $nodes['a']->addEdge($nodes['d']); // d depends on a + $nodes['b']->addEdge($nodes['c']); // c depends on b + $nodes['b']->addEdge($nodes['e']); // e depends on b + $nodes['c']->addEdge($nodes['d']); // d depends on c + $nodes['c']->addEdge($nodes['e']); // e depends on c + + $class = new Resolver(); + $class->resolve($nodes['a']); + + $this->assertSame([ + 'memory_a' => ['memory_d','memory_e','memory_c','memory_b'], + 'memory_b' => ['memory_d','memory_e','memory_c'], + 'memory_c' => ['memory_d','memory_e'], + 'memory_d' => [], + 'memory_e' => [], + ], array_map(function(array $v){ + return array_map(function(Node $n){ + return $n->getUid(); + }, $v); + }, $class->getAdjacencyList())); + } catch (\Exception $e) { + $this->fail($e); + return; + } + } + + public function testGraphReduction() + { + try{ + /** @var MemorySource[] $nodes */ + $nodes = []; + foreach (range('a', 'f') as $letter) { + $nodes[$letter] = new MemorySource('memory_' . $letter, 'Howdy!', 'memory.md', 'md', 'memory/' . $letter, 'memory/' . $letter . '/memory.md'); + } + + $nodes['c']->setHasChanged(); + + $nodes['a']->addEdge($nodes['b']); // b depends on a + $nodes['a']->addEdge($nodes['d']); // d depends on a + $nodes['b']->addEdge($nodes['c']); // c depends on b + $nodes['b']->addEdge($nodes['e']); // e depends on b + $nodes['c']->addEdge($nodes['d']); // d depends on c + $nodes['c']->addEdge($nodes['e']); // e depends on c + + $reduce = function (AbstractSource $source) { + return $source->hasChanged(); + }; + + $class = new Resolver(); + $class->resolve($nodes['a']); + $reduced = $class->reduce($reduce); + + $this->assertCount(3, $reduced); + $this->assertSame([$nodes['c'],$nodes['d'], $nodes['e']], $reduced); + + $nodes['e']->addEdge($nodes['f']); // f depends on e + $class = new Resolver(); + $class->resolve($nodes['a']); + $reduced = $class->reduce($reduce); + + $this->assertCount(4, $reduced); + $this->assertSame([$nodes['c'],$nodes['d'], $nodes['f'], $nodes['e']], $reduced); + } catch (\Exception $e) { + $this->fail($e); + return; + } + } +} \ No newline at end of file diff --git a/tests/Unit/FileSystemCollectorExclusionNTest.php b/tests/Unit/FileSystemCollectorExclusionNTest.php new file mode 100644 index 0000000..8e73f89 --- /dev/null +++ b/tests/Unit/FileSystemCollectorExclusionNTest.php @@ -0,0 +1,46 @@ +mockMemorySource('not-a-draft'); + + $this->assertFalse($exclusion->filter($file)); + + $file->setData('draft', true); + $this->assertTrue($exclusion->filter($file)); + + $exclusion = new DraftsExclusion(true); + $file = $this->mockMemorySource('not-a-draft'); + + $this->assertFalse($exclusion->filter($file)); + + $file->setData('draft', true); + $this->assertFalse($exclusion->filter($file)); + } + + public function testPathExclusion() + { + $exclusion = new PathExclusion('_assets'); + + $file = $this->mockMemorySource('test-assert', 'css'); + $file->setOverloaded('relativePath', '_assets/css'); + + $this->assertTrue($exclusion->filter($file)); + + $file->setOverloaded('relativePath', 'css/_assets'); + $this->assertTrue($exclusion->filter($file)); + + $file->setOverloaded('relativePath', 'somewhere/else'); + $this->assertFalse($exclusion->filter($file)); + } +} \ No newline at end of file diff --git a/tests/Unit/FileSystemCollectorMutatorNTest.php b/tests/Unit/FileSystemCollectorMutatorNTest.php new file mode 100644 index 0000000..7362c6f --- /dev/null +++ b/tests/Unit/FileSystemCollectorMutatorNTest.php @@ -0,0 +1,129 @@ +mockSplFileSource(__DIR__ . '/../Mocks/TestFile.md', 'Mocks', 'Mocks/TestFile.md'); + $this->assertCount(1, $file->getData()); + + $mutator->mutate($file); + $this->assertCount(4, $file->getData()); + $this->assertSame('This is a test file...', trim($file->getRenderedContent())); + } + + /** + * @throws \Exception + */ + public function testIsDraftMutator() + { + $mutator = new IsScheduledMutator(true,true); + + $file = $this->mockMemorySource('TestFile'); + $file->setData([ + 'draft' => true, + 'date' => new DateTime('01-01-2015') + ]); + + $mutator->mutate($file); + $this->assertTrue($file->getData('draft')); + + // Scheduled Source that should be published (publish date in past, draft set to true) + $mutator = new IsScheduledMutator(false,true); + $mutator->mutate($file); + $this->assertFalse($file->getData('draft')); + + // Scheduled Source that shouold not be published (publish date in future, draft set to true) + $file = $this->mockMemorySource('TestFile'); + $file->setData([ + 'draft' => true, + 'date' => new DateTime('01-01-2099') + ]); + + $mutator->mutate($file); + $this->assertTrue($file->getData('draft')); + + // Disabled + $mutator = new IsScheduledMutator(); + $file = $this->mockMemorySource('TestFile'); + $file->setData([ + 'draft' => true, + 'date' => new DateTime('01-01-2015') + ]); + $mutator->mutate($file); + $this->assertTrue($file->getData('draft')); + } + + public function testisIgnoredMutator() + { + $mutator = new IsIgnoredMutator(['_views', '_templates'], ['_blog']); + + $file = $this->mockMemorySource('test-file'); + $file->setOverloaded('relativePath', 'abc/123'); + $mutator->mutate($file); + $this->assertFalse($file->isIgnored()); + + $file = $this->mockMemorySource('test-file'); + $file->setOverloaded('relativePath', '_views/abc/123'); + $mutator->mutate($file); + $this->assertTrue($file->isIgnored()); + + $file = $this->mockMemorySource('test-file'); + $file->setOverloaded('relativePath', '_templates/abc/123'); + $mutator->mutate($file); + $this->assertTrue($file->isIgnored()); + + $file = $this->mockMemorySource('test-file'); + $file->setOverloaded('relativePath', '_blog/2017'); + $mutator->mutate($file); + $this->assertFalse($file->isIgnored()); + + $file = $this->mockMemorySource('test-file'); + $file->setOverloaded('relativePath', 'abc/123/_test'); + $mutator->mutate($file); + $this->assertTrue($file->isIgnored()); + } + + public function testSetDateDataFromFileNameMutator() + { + $mutator = new SetDateDataFromFileNameMutator(); + + $file = $this->mockSplFileSource(__DIR__ . '/../Mocks/TestFile.md', 'Mocks', 'Mocks/TestFile.md'); + $mutator->mutate($file); + + $this->assertCount(1, $file->getData()); + + $file = $this->mockSplFileSource(__DIR__ . '/../Mocks/2018-02-01-this-is-a-test.md', 'Mocks', 'Mocks/2018-02-01-this-is-a-test.md'); + $mutator->mutate($file); + + $this->assertCount(4, $file->getData()); + $this->assertSame('this-is-a-test', $file->getData('slug')); + $this->assertSame('This is a test', $file->getData('title')); + $this->assertInstanceOf(DateTime::class, $file->getData('date')); + $this->assertSame('2018-02-01', $file->getData('date')->format('Y-m-d')); + + $file = $this->mockSplFileSource(__DIR__ . '/../Mocks/01-02-2018-this-is-a-test.md', 'Mocks', 'Mocks/01-02-2018-this-is-a-test.md'); + $mutator->mutate($file); + + $this->assertCount(4, $file->getData()); + $this->assertSame('this-is-a-test', $file->getData('slug')); + $this->assertSame('This is a test', $file->getData('title')); + $this->assertInstanceOf(DateTime::class, $file->getData('date')); + $this->assertSame('2018-02-01', $file->getData('date')->format('Y-m-d')); + } +} \ No newline at end of file diff --git a/tests/Unit/FilesystemCollectorNTest.php b/tests/Unit/FilesystemCollectorNTest.php new file mode 100644 index 0000000..39dba2b --- /dev/null +++ b/tests/Unit/FilesystemCollectorNTest.php @@ -0,0 +1,58 @@ +expectException(\Exception::class); + $this->expectExceptionMessage('The source path [does-not-exist] could not be read or does not exist.'); + + new FilesystemCollector('does-not-exist'); + } + + /** + * Tests the FilesystemCollector with a handful of loosely configured mutators and exclusions. + */ + public function testFilesystemCollector() + { + $this->loadToTmp($this->assetPath('build_test_41/src')); + + try { + $class = new FilesystemCollector( + $this->assetPath('build_test_41/src/source'), + [ + new SetDateDataFromFileNameMutator(), + new FrontMatterMutator(), + new IsScheduledMutator(), + new IsIgnoredMutator(['_views', '_templates'], ['_blog']), + ], + [ + new DraftsExclusion(), + new PathExclusion('ignored_folder') + ] + ); + + $arr = $class->collect(); + $this->assertTrue(is_array($arr)); + $this->assertCount(9, $arr); + } catch (\Exception $e) { + $this->fail($e); + return; + } + } +} \ No newline at end of file diff --git a/tests/Unit/FrontMatterNTest.php b/tests/Unit/FrontMatterNTest.php index d23a6e1..f0537ef 100644 --- a/tests/Unit/FrontMatterNTest.php +++ b/tests/Unit/FrontMatterNTest.php @@ -2,9 +2,9 @@ namespace Tapestry\Tests\Unit; -use Tapestry\Entities\ProjectFile; use Symfony\Component\Finder\SplFileInfo; use Tapestry\Modules\Content\FrontMatter; +use Tapestry\Modules\Source\SplFileSource; use Tapestry\Tests\TestCase; class FrontMatterNTest extends TestCase @@ -15,26 +15,36 @@ class FrontMatterNTest extends TestCase */ function testFrontMatterParsedWhenBodyEmpty() { - $file = new ProjectFile(new SplFileInfo(__DIR__ . '/../Mocks/TestFileNoBody.md', '', '')); - $frontMatter = new FrontMatter($file->getFileContent()); - $this->assertSame('', $frontMatter->getContent()); - $this->assertSame([ - 'title' => 'Test File Title', - 'draft' => false, - 'date' => 507600000 - ], $frontMatter->getData()); + try { + $file = new SplFileSource(new SplFileInfo(__DIR__ . '/../Mocks/TestFileNoBody.md', '', '')); + $frontMatter = new FrontMatter($file->getRawContent()); + $this->assertSame('', $frontMatter->getContent()); + $this->assertSame([ + 'title' => 'Test File Title', + 'draft' => false, + 'date' => 507600000 + ], $frontMatter->getData()); + } catch (\Exception $e) { + $this->fail($e); + return; + } } function testFrontMatterAndBodyParsedCorrectly() { - $file = new ProjectFile(new SplFileInfo(__DIR__ . '/../Mocks/TestFile.md', '', '')); - $frontMatter = new FrontMatter($file->getFileContent()); + try { + $file = new SplFileSource(new SplFileInfo(__DIR__ . '/../Mocks/TestFile.md', '', '')); + $frontMatter = new FrontMatter($file->getRawContent()); $this->assertSame('This is a test file...', $frontMatter->getContent()); $this->assertSame([ 'title' => 'Test File Title', 'draft' => false, 'date' => 507600000 ], $frontMatter->getData()); + } catch (\Exception $e) { + $this->fail($e); + return; + } } function testFrontMatterParsedWhenEmpty() diff --git a/tests/Unit/GraphNTest.php b/tests/Unit/GraphNTest.php new file mode 100644 index 0000000..b3e0046 --- /dev/null +++ b/tests/Unit/GraphNTest.php @@ -0,0 +1,58 @@ +addEdge('a', $nodes['b']); // b depends on a + $graph->addEdge('a', $nodes['d']); // d depends on a + $graph->addEdge('b', $nodes['c']); // c depends on b + $graph->addEdge('b', $nodes['e']); // e depends on b + $graph->addEdge('c', $nodes['d']); // d depends on c + $graph->addEdge('c', $nodes['e']); // e depends on c + $graph->addEdge('a', $nodes['f']); // e depends on c + + foreach (range('a', 'f') as $letter) { + $this->assertSame($nodes[$letter], $graph->getEdge($letter)); + } + + $this->assertCount(3, $graph->getEdge('a')->getEdges()); + } catch (\Exception $e) { + $this->fail($e); + return; + } + } + + /** + * Unit Test of the Graph class exception state. + * + * @throws \Tapestry\Exceptions\GraphException + */ + public function testGraphClassExceptionThrown() + { + $graph = new Graph(); + $this->expectExceptionMessage('The edge [a] is not found in graph.'); + $graph->addEdge('a', new SimpleNode('temp', 'temp')); + + $graph = new Graph(); + $this->expectExceptionMessage('The edge [a] is not found in graph.'); + $graph->getEdge('a'); + } +} diff --git a/tests/Unit/MemoryCollectorNTest.php b/tests/Unit/MemoryCollectorNTest.php new file mode 100644 index 0000000..fad2314 --- /dev/null +++ b/tests/Unit/MemoryCollectorNTest.php @@ -0,0 +1,62 @@ + 'test-file_md', + 'rawContent' => 'Hello World!', + 'filename' => 'test-file.md', + 'ext' => 'md', + 'relativePath' => '_blog', + 'relativePathname' => '_blog/test-file.md' + ], + [ + 'uid' => 'test-file-2_md', + 'rawContent' => 'Hello World!', + 'filename' => 'test-file-2.md', + 'ext' => 'md', + 'relativePath' => '_blog', + 'relativePathname' => '_blog/test-file-2.md', + 'data' => [ + 'draft' => true + ] + ] + ], + [ + new SetDateDataFromFileNameMutator(), + new FrontMatterMutator(), + new IsScheduledMutator(), + new IsIgnoredMutator(['_views', '_templates'], ['_blog']), + ], + [ + new DraftsExclusion(), + new PathExclusion('ignored_folder') + ] + ); + + $arr = $class->collect(); + $this->assertTrue(is_array($arr)); + $this->assertCount(1, $arr); + } catch (\Exception $e) { + $this->fail($e); + return; + } + } +} \ No newline at end of file diff --git a/tests/Unit/MemorySourceNTest.php b/tests/Unit/MemorySourceNTest.php new file mode 100644 index 0000000..39f744f --- /dev/null +++ b/tests/Unit/MemorySourceNTest.php @@ -0,0 +1,139 @@ +assertSame('/memory/123/memory/index.md', $class->getCompiledPermalink()); + } catch (\Exception $e) { + $this->fail($e); + return; + } + + $this->assertSame('md', $class->getExtension()); + $this->assertSame('memory.md', $class->getFilename()); + $this->assertSame('memory', $class->getBasename()); + $this->assertSame('memory_123', $class->getUid()); + $this->assertFalse($class->hasData('hello-world')); + $this->assertFalse($class->hasContent()); + $this->assertFalse($class->hasChanged()); + $this->assertFalse($class->isRendered()); + $this->assertFalse($class->isToCopy()); + $this->assertFalse($class->isIgnored()); + $this->assertInstanceOf(Permalink::class, $class->getPermalink()); + + $class->setHasChanged(); + $this->assertTrue($class->hasChanged()); + + $class->setRendered(); + $this->assertTrue($class->isRendered()); + + $class->setToCopy(); + $this->assertTrue($class->isToCopy()); + + $class->setIgnored(); + $this->assertTrue($class->isIgnored()); + + $this->assertSame(['uid' => 'memory_123'], $class->getData()); + + try { + $class->setDataFromArray([ + 'a' => 123, + 'b' => 'abc', + 'c' => 3.14, + + ]); + + $class->setData('d', 'Hello World'); + $class->setData(['e' => 'elephant', 'f' => 'flamingo']); + } catch (\Exception $e){ + $this->fail($e); + return; + } + + $this->assertSame(['uid' => 'memory_123', 'a' => 123, 'b' => 'abc', 'c' => 3.14, 'd' => 'Hello World', 'e' => 'elephant', 'f' => 'flamingo'], $class->getData()); + + try{ + $class->setData('date', '11th September 2001'); + $class->setData('permalink', '/abc/123/xyz.html'); + } catch (\Exception $e) { + $this->fail($e); + return; + } + + /** @var \DateTime $date */ + $date = $class->getData('date'); + $this->assertInstanceOf(\DateTime::class, $date); + $this->assertSame('11-09-2001', $date->format('d-m-Y')); + + try { + $this->assertSame('/abc/123/xyz.html', $class->getCompiledPermalink()); + }catch (\Exception $e) { + $this->fail($e); + return; + } + + try { + $class->setData('date', 'elephants'); + }catch (\Exception $e) { + $this->assertSame('The date [elephants] is in a format not supported by Tapestry.', $e->getMessage()); + } + + $this->assertSame('11-09-2001', $date->format('d-m-Y')); + + // + // Overloaded + // + + $class->setOverloaded('ext', 'phtml'); + $this->assertSame('phtml', $class->getExtension()); + $this->assertSame('md', $class->getExtension(false)); + $this->assertSame('memory', $class->getBasename()); + $this->assertSame('memory', $class->getBasename(false)); + + $class->setOverloaded('filename', 'hello-world.phtml'); + $this->assertSame('hello-world.phtml', $class->getFilename()); + $this->assertSame('memory.md', $class->getFilename(false)); + $this->assertSame('hello-world', $class->getBasename()); + $this->assertSame('memory', $class->getBasename(false)); + + $class->setOverloaded('relativePath', 'abc/123'); + $this->assertSame('abc/123', $class->getRelativePath()); + $this->assertSame('memory/123', $class->getRelativePath(false)); + + $class->setOverloaded('relativePathname', 'abc/123/hello-world.phtml'); + $this->assertSame('abc/123/hello-world.phtml', $class->getRelativePathname()); + $this->assertSame('memory/123/memory.md', $class->getRelativePathname(false)); + + try{ + $this->assertSame('Howdy!', $class->getRawContent()); + } catch (\Exception $e) { + $this->fail($e); + return; + } + + try{ + $class->getRenderedContent(); + } catch (\Exception $e) { + $this->assertSame('The file [abc/123/hello-world.phtml] has not been loaded.', $e->getMessage()); + } + + $class->setRenderedContent('Hello World!'); + $this->assertSame('Hello World!', $class->getRenderedContent()); + } + +} \ No newline at end of file diff --git a/tests/Unit/SimpleNodeNTest.php b/tests/Unit/SimpleNodeNTest.php new file mode 100644 index 0000000..7a7b0bc --- /dev/null +++ b/tests/Unit/SimpleNodeNTest.php @@ -0,0 +1,37 @@ +assertEquals('test', $class->getUid()); + $this->assertEquals('hello world', $class->getHash()); + $this->assertEquals([], $class->getEdges()); + + $this->assertTrue($class->isSame($class)); + $this->assertTrue($class->isSame(new SimpleNode('test', 'hello world'))); + } + + /** + * Unit Test of the SimpleNode class exception state. + * + * @throws \Tapestry\Exceptions\GraphException + */ + public function testSimpleNodeClassException() + { + $class = new SimpleNode('test', 'hello world'); + $this->expectExceptionMessage('Node being compared must have the same identifier.'); + $this->assertFalse($class->isSame(new SimpleNode('hello world', 'test'))); + } +} diff --git a/tests/Unit/SplFileSourceNTest.php b/tests/Unit/SplFileSourceNTest.php new file mode 100644 index 0000000..779f40d --- /dev/null +++ b/tests/Unit/SplFileSourceNTest.php @@ -0,0 +1,140 @@ +assertSame('/Mocks/testfilenobody/index.md', $class->getCompiledPermalink()); + } catch (\Exception $e) { + $this->fail($e); + return; + } + + $this->assertSame('md', $class->getExtension()); + $this->assertSame('TestFileNoBody.md', $class->getFilename()); + $this->assertSame('TestFileNoBody', $class->getBasename()); + $this->assertSame('Mocks_TestFileNoBody_md', $class->getUid()); + $this->assertFalse($class->hasData('hello-world')); + $this->assertFalse($class->hasContent()); + $this->assertFalse($class->hasChanged()); + $this->assertFalse($class->isRendered()); + $this->assertFalse($class->isToCopy()); + $this->assertFalse($class->isIgnored()); + $this->assertInstanceOf(Permalink::class, $class->getPermalink()); + + $class->setHasChanged(); + $this->assertTrue($class->hasChanged()); + + $class->setRendered(); + $this->assertTrue($class->isRendered()); + + $class->setToCopy(); + $this->assertTrue($class->isToCopy()); + + $class->setIgnored(); + $this->assertTrue($class->isIgnored()); + + $this->assertSame(['uid' => 'Mocks_TestFileNoBody_md'], $class->getData()); + + try { + $class->setDataFromArray([ + 'a' => 123, + 'b' => 'abc', + 'c' => 3.14, + + ]); + + $class->setData('d', 'Hello World'); + $class->setData(['e' => 'elephant', 'f' => 'flamingo']); + } catch (\Exception $e){ + $this->fail($e); + return; + } + + $this->assertSame(['uid' => 'Mocks_TestFileNoBody_md', 'a' => 123, 'b' => 'abc', 'c' => 3.14, 'd' => 'Hello World', 'e' => 'elephant', 'f' => 'flamingo'], $class->getData()); + + try{ + $class->setData('date', '11th September 2001'); + $class->setData('permalink', '/abc/123/xyz.html'); + } catch (\Exception $e) { + $this->fail($e); + return; + } + + /** @var \DateTime $date */ + $date = $class->getData('date'); + $this->assertInstanceOf(\DateTime::class, $date); + $this->assertSame('11-09-2001', $date->format('d-m-Y')); + + try { + $this->assertSame('/abc/123/xyz.html', $class->getCompiledPermalink()); + }catch (\Exception $e) { + $this->fail($e); + return; + } + + try { + $class->setData('date', 'elephants'); + }catch (\Exception $e) { + $this->assertSame('The date [elephants] is in a format not supported by Tapestry.', $e->getMessage()); + } + + $this->assertSame('11-09-2001', $date->format('d-m-Y')); + + // + // Overloaded + // + + $class->setOverloaded('ext', 'phtml'); + $this->assertSame('phtml', $class->getExtension()); + $this->assertSame('md', $class->getExtension(false)); + $this->assertSame('TestFileNoBody', $class->getBasename()); + $this->assertSame('TestFileNoBody', $class->getBasename(false)); + + $class->setOverloaded('filename', 'hello-world.phtml'); + $this->assertSame('hello-world.phtml', $class->getFilename()); + $this->assertSame('TestFileNoBody.md', $class->getFilename(false)); + $this->assertSame('hello-world', $class->getBasename()); + $this->assertSame('TestFileNoBody', $class->getBasename(false)); + + $class->setOverloaded('relativePath', 'abc/123'); + $this->assertSame('abc/123', $class->getRelativePath()); + $this->assertSame('Mocks', $class->getRelativePath(false)); + + $class->setOverloaded('relativePathname', 'abc/123/hello-world.phtml'); + $this->assertSame('abc/123/hello-world.phtml', $class->getRelativePathname()); + $this->assertSame('Mocks/TestFileNoBody.md', $class->getRelativePathname(false)); + + try{ + $this->assertSame(file_get_contents(__DIR__ . '/../Mocks/TestFileNoBody.md'), $class->getRawContent()); + } catch (\Exception $e) { + $this->fail($e); + return; + } + + try{ + $class->getRenderedContent(); + } catch (\Exception $e) { + $this->assertSame('The file [abc/123/hello-world.phtml] has not been loaded.', $e->getMessage()); + } + + $class->setRenderedContent('Hello World!'); + $this->assertSame('Hello World!', $class->getRenderedContent()); + } + +} \ No newline at end of file diff --git a/tests/Unit/TreeNTest.php b/tests/Unit/TreeNTest.php new file mode 100644 index 0000000..158befc --- /dev/null +++ b/tests/Unit/TreeNTest.php @@ -0,0 +1,200 @@ +add($root); + $this->assertSame($root, $tree->getRoot()); + + $rootLeafA = new Leaf('root.a', new Symbol('Root_Leaf_A', Symbol::SYMBOL_CONTENT_TYPE, 100)); + $tree->add($rootLeafA, 'root'); + $rootLeafB = new Leaf('root.b', new Symbol('Root_Leaf_B', Symbol::SYMBOL_CONTENT_TYPE, 100)); + $tree->add($rootLeafB, 'root'); + $rootLeafBC = new Leaf('root.b.c', new Symbol('Root_Leaf_B_C', Symbol::SYMBOL_SOURCE, 100)); + $tree->add($rootLeafBC, 'root.b'); + + $this->assertEquals(2, $tree->getRoot()->childCount()); + + $check = ['root', 'root.a', 'root.b', 'root.b.c']; + $arr = []; + + $tree->traverse(function (Leaf $leaf) use ($check, &$arr) { + $arr[] = $leaf->getId(); + $this->assertTrue(in_array($leaf->getId(), $check)); + }); + + $this->assertCount(4, $arr); + $this->assertEquals(4, $tree->childCount()); + } + + /** + * Example AST Tree: + * + * ├── kernel.php + * | ├── Content Type A + * | | ├── File A + * | | ├── File B + * | | └── File C + * | ├── Content Type B + * | | ├── File D + * | | └── File E + * | ├── Template A + * | | ├── File A + * | | ├── View A + * | | | ├── File B + * | | | └── File C + * | | └── File D + * | └── Template B + * | └── File E + * └── config.php + * └── *All the same nodes as kernel.php + * + * If only Template B changes then only File E needs to be re-generated and the tree will reduce to: + * + * └── Template B + * └── File E + * + * Resulting in only one file needing to be regenerated. + * + * Once generated the AST will be cached by Tapestry and amended as files are added/removed from + * the project workspace. + */ + public function testASTReduce() + { + $treeA = new Tree(); + $treeA->add(new Leaf('kernel', new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $treeA->add(new Leaf('kernel.config', new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 100)), 'kernel'); + $treeA->add(new Leaf('kernel.config.content_type_blog', new Symbol('content_type_blog', Symbol::SYMBOL_CONTENT_TYPE, 100)), 'kernel.config'); + $treeA->add(new Leaf('kernel.config.content_type_blog.blog_view', new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog'); + $treeA->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_a', new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + $treeA->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_b', new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + + $treeB = new Tree(); + $treeB->add(new Leaf('kernel', new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $treeB->add(new Leaf('kernel.config', new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 100)), 'kernel'); + $treeB->add(new Leaf('kernel.config.content_type_blog', new Symbol('content_type_blog', Symbol::SYMBOL_CONTENT_TYPE, 100)), 'kernel.config'); + $treeB->add(new Leaf('kernel.config.content_type_blog.blog_view', new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 150)), 'kernel.config.content_type_blog'); + $treeB->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_a', new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + $treeB->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_b', new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + + $shaker = new TreeShaker(); + $list = $shaker->reduce($treeA, $treeB); + $this->assertEquals(3, count($list)); + + $treeC = new Tree(); + $treeC->add(new Leaf('kernel', new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $treeC->add(new Leaf('kernel.config', new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 100)), 'kernel'); + $treeC->add(new Leaf('kernel.config.content_type_blog', new Symbol('content_type_blog', Symbol::SYMBOL_CONTENT_TYPE, 100)), 'kernel.config'); + $treeC->add(new Leaf('kernel.config.content_type_blog.blog_view', new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog'); + $treeC->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_a', new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + $treeC->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_b', new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + + $list = $shaker->reduce($treeA, $treeC); + $this->assertEquals(0, count($list)); + + $treeD = new Tree(); + $treeD->add(new Leaf('kernel', new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $treeD->add(new Leaf('kernel.config', new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 150)), 'kernel'); + $treeD->add(new Leaf('kernel.config.content_type_blog', new Symbol('content_type_blog', Symbol::SYMBOL_CONTENT_TYPE, 100)), 'kernel.config'); + $treeD->add(new Leaf('kernel.config.content_type_blog.blog_view', new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog'); + $treeD->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_a', new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + $treeD->add(new Leaf('kernel.config.content_type_blog.blog_view.blog_page_b', new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100)), 'kernel.config.content_type_blog.blog_view'); + + $list = $shaker->reduce($treeA, $treeD); + $this->assertEquals(5, count($list)); + } + + public function testAddSymbol() + { + $treeA = new Tree(); + $this->assertTrue($treeA->addSymbol(new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $this->assertTrue($treeA->addSymbol(new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 100), 'kernel')); + $this->assertTrue($treeA->addSymbol(new Symbol('content_type_blog', Symbol::SYMBOL_CONTENT_TYPE, 100), 'configuration')); + $this->assertTrue($treeA->addSymbol(new Symbol('content_type_default', Symbol::SYMBOL_CONTENT_TYPE, 100), 'configuration')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100), 'content_type_blog')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_c', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_d', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('template_a', Symbol::SYMBOL_SOURCE, 100), 'content_type_default')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100), 'template_a')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_c', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_d', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeA->addSymbol(new Symbol('blog_page_e', Symbol::SYMBOL_SOURCE, 100), 'template_a')); + + echo 'Tree A' . PHP_EOL; + echo (new TreeToASCII($treeA)); + + $treeB = new Tree(); + $this->assertTrue($treeB->addSymbol(new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $this->assertTrue($treeB->addSymbol(new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 100), 'kernel')); + $this->assertTrue($treeB->addSymbol(new Symbol('content_type_blog', Symbol::SYMBOL_CONTENT_TYPE, 100), 'configuration')); + $this->assertTrue($treeB->addSymbol(new Symbol('content_type_default', Symbol::SYMBOL_CONTENT_TYPE, 100), 'configuration')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100), 'content_type_blog')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_c', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_d', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('template_a', Symbol::SYMBOL_SOURCE, 150), 'content_type_default')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_view', Symbol::SYMBOL_SOURCE, 100), 'template_a')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_a', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_b', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_c', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_d', Symbol::SYMBOL_SOURCE, 100), 'blog_view')); + $this->assertTrue($treeB->addSymbol(new Symbol('blog_page_e', Symbol::SYMBOL_SOURCE, 100), 'template_a')); + + $shaker = new TreeShaker(); + $list = $shaker->reduce($treeA, $treeB); + $this->assertEquals(7, count($list)); + } + + /** + * The AST can have duplicate references but the structure of each should be identical... + * + * For example + * └──kernel + * └──configuration + * └──content_type.default + * ├──about_md + * ├──index_phtml + * ├──_templates_default_phtml + * └──index_phtml + * └──_templates_sidebar_phtml + * └──_templates_default_phtml + * + * The above Tree is invalid because _templates_default_phtml is referenced twice + * but both references have a different child structure. + * + * + */ + public function testAddDuplicates() + { + + $tree = new Tree(); + $this->assertTrue($tree->addSymbol(new Symbol('kernel', Symbol::SYMBOL_KERNEL, 100))); + $this->assertTrue($tree->addSymbol(new Symbol('configuration', Symbol::SYMBOL_CONFIGURATION, 100), 'kernel')); + $this->assertTrue($tree->addSymbol(new Symbol('content_type.default', Symbol::SYMBOL_CONTENT_TYPE, 100), 'configuration')); + $this->assertTrue($tree->addSymbol(new Symbol('about_md', Symbol::SYMBOL_CONTENT_TYPE, 100), 'content_type.default')); + $this->assertTrue($tree->addSymbol(new Symbol('index_phtml', Symbol::SYMBOL_CONTENT_TYPE, 100), 'content_type.default')); + $this->assertTrue($tree->addSymbol(new Symbol('_templates_default_phtml', Symbol::SYMBOL_CONTENT_TYPE, 100), 'content_type.default')); + $this->assertTrue($tree->addSymbol(new Symbol('_templates_sidebar_phtml', Symbol::SYMBOL_CONTENT_TYPE, 100), 'content_type.default')); + $this->assertTrue($tree->addSymbol(new Symbol('index_phtml', Symbol::SYMBOL_CONTENT_TYPE, 100), '_templates_default_phtml')); + $this->assertTrue($tree->addSymbol(new Symbol('_templates_default_phtml', Symbol::SYMBOL_CONTENT_TYPE, 100), '_templates_sidebar_phtml')); + + echo (new TreeToASCII($tree)); + } +} \ No newline at end of file diff --git a/tests/assets/build_test_41/src/source/_blog/2016-03-10-test-blog-entry.md b/tests/assets/build_test_41/src/source/_blog/2016-03-10-test-blog-entry.md index afaaaae..bdfb7ff 100644 --- a/tests/assets/build_test_41/src/source/_blog/2016-03-10-test-blog-entry.md +++ b/tests/assets/build_test_41/src/source/_blog/2016-03-10-test-blog-entry.md @@ -1,5 +1,7 @@ --- title: This is a test blog entry +categories: + - test --- # This is a test posting diff --git a/tests/assets/build_test_41/src/source/_blog/2016-03-11-test-blog-entry-two.md b/tests/assets/build_test_41/src/source/_blog/2016-03-11-test-blog-entry-two.md index fcde37d..2d3a524 100644 --- a/tests/assets/build_test_41/src/source/_blog/2016-03-11-test-blog-entry-two.md +++ b/tests/assets/build_test_41/src/source/_blog/2016-03-11-test-blog-entry-two.md @@ -1,5 +1,8 @@ --- title: This is another test blog entry +draft: true +categories: + - test --- # This is a test posting diff --git a/tests/assets/build_test_41/src/source/_templates/default.phtml b/tests/assets/build_test_41/src/source/_templates/default.phtml index c19eb77..852b6d2 100644 --- a/tests/assets/build_test_41/src/source/_templates/default.phtml +++ b/tests/assets/build_test_41/src/source/_templates/default.phtml @@ -8,12 +8,13 @@