├── .gitignore
├── .travis.yml
├── .travis
└── secrets.tar.enc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin
├── deploy.sh
├── handle
└── parse-manifest.php
├── box.json
├── composer.json
├── init-structure
├── _cache
│ └── .gitkeep
├── _content
│ ├── about.md
│ └── index.md
├── _themes
│ └── default
│ │ ├── index.blade.php
│ │ └── layout.blade.php
└── config.yml
├── phpunit.xml
├── src
└── Commands
│ ├── BuildCommand.php
│ ├── InitCommand.php
│ ├── RollbackCommand.php
│ └── UpdateCommand.php
└── tests
├── Commands
├── BuildCommandTest.php
└── InitCommandTest.php
├── HandleTestCase.php
├── bootstrap.php
└── output
└── .gitignore
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | /init-structure/themes/default/cache
4 | composer.lock
5 | .travis/*.pem
6 | .travis/secrets.tar
7 | handle.phar
8 | handle.phar.pubkey
9 | handle.phar.version
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 5.6
4 | - 7.0
5 |
6 | matrix:
7 | include:
8 | - php: 7.0
9 | env:
10 | - EXECUTE_DEPLOYMENT=true
11 |
12 | before_install:
13 | - openssl aes-256-cbc -K $encrypted_9bbf475f87f0_key -iv $encrypted_9bbf475f87f0_iv -in .travis/secrets.tar.enc -out .travis/secrets.tar -d
14 |
15 | before_script:
16 | - composer self-update
17 | - composer install --no-interaction
18 |
19 | after_success:
20 | - if [[ $EXECUTE_DEPLOYMENT == 'true' && $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then composer install --no-dev ; fi
21 | - if [[ $EXECUTE_DEPLOYMENT == 'true' && $TRAVIS_BRANCH == 'master' && $TRAVIS_PULL_REQUEST == 'false' ]]; then ./bin/deploy.sh ; fi
--------------------------------------------------------------------------------
/.travis/secrets.tar.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gilbitron/Handle/8c5d553729f2b4b82622543e25c9804f902b9cab/.travis/secrets.tar.enc
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Handle Changelog
2 |
3 | Version 0.1.4 - 2016.06.20
4 | --------------------------
5 | * New: Added `slug` template variable
6 | * New: Added `build_path` template variable
7 |
8 | Version 0.1.3 - 2016.06.14
9 | --------------------------
10 | * Changed: Include content meta in templates
11 |
12 | Version 0.1.2 - 2016.06.13
13 | --------------------------
14 | * Changed: Improved the `build --watch` implementation
15 | * Changed: Build only required content when content changes
16 |
17 | Version 0.1.1 - 2016.06.02
18 | --------------------------
19 | * New: Pretty permalinks (removed .htaccess)
20 |
21 | Version 0.1.0 - 2016.04.12
22 | --------------------------
23 | * Initial release
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Gilbert Pellegrom
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/gilbitron/Handle)
2 | [](LICENSE)
3 |
4 | # Handle
5 |
6 | A static site generator powered by PHP and the command line.
7 |
8 | ## Documentation
9 |
10 | See [handlecli.com](https://handlecli.com/) for full documentation.
11 |
12 | ## Credits
13 |
14 | Handle was created by [Gilbert Pellegrom](http://gilbert.pellegrom.me) from
15 | [Dev7studios](http://dev7studios.co). Released under the MIT license.
16 |
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Unpack secrets; -C ensures they unpack *in* the .travis directory
4 | tar xvf .travis/secrets.tar -C .travis
5 |
6 | # Setup SSH agent:
7 | eval "$(ssh-agent -s)" #start the ssh agent
8 | chmod 600 .travis/build-key.pem
9 | ssh-add .travis/build-key.pem
10 |
11 | # Setup git defaults:
12 | git config --global user.email "gilbert@pellegrom.me"
13 | git config --global user.name "Gilbert Pellegrom"
14 |
15 | # Add SSH-based remote to GitHub repo:
16 | git remote add handle git@github.com:gilbitron/Handle.git
17 | git fetch handle
18 |
19 | # Get box and build PHAR
20 | wget https://box-project.github.io/box2/manifest.json
21 | BOX_URL=$(php bin/parse-manifest.php manifest.json)
22 | rm manifest.json
23 | wget -O box.phar ${BOX_URL}
24 | chmod 755 box.phar
25 | ./box.phar build -vv
26 | # Without the following step, we cannot checkout the gh-pages branch due to
27 | # file conflicts:
28 | mv handle.phar handle.phar.tmp
29 |
30 | # Checkout gh-pages and add PHAR file and version:
31 | git checkout -b gh-pages handle/gh-pages
32 | mv handle.phar.tmp handle.phar
33 | sha1sum handle.phar > handle.phar.version
34 | git add handle.phar handle.phar.version
35 |
36 | # Create download bundle
37 | tar -zcvf handle.tar.gz handle.phar handle.phar.pubkey
38 | git add handle.tar.gz
39 |
40 | # Commit and push:
41 | git commit -m 'Rebuilt phar'
42 | git push handle gh-pages:gh-pages
--------------------------------------------------------------------------------
/bin/handle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new \Handle\Commands\InitCommand());
10 | $app->add(new \Handle\Commands\BuildCommand());
11 | $app->add(new \Handle\Commands\UpdateCommand());
12 | $app->add(new \Handle\Commands\RollbackCommand());
13 | $app->run();
--------------------------------------------------------------------------------
/bin/parse-manifest.php:
--------------------------------------------------------------------------------
1 | =')) {
23 | echo $file['url'];
24 | exit(0);
25 | }
26 | }
27 |
28 | echo $fallbackUrl;
29 | exit(0);
--------------------------------------------------------------------------------
/box.json:
--------------------------------------------------------------------------------
1 | {
2 | "algorithm": "OPENSSL",
3 | "chmod": "0755",
4 | "compression": "GZ",
5 | "directories": [
6 | "init-structure",
7 | "src"
8 | ],
9 | "files": [
10 | "LICENSE"
11 | ],
12 | "finder": [
13 | {
14 | "name": "*.php",
15 | "exclude": [
16 | "test",
17 | "tests",
18 | "Test",
19 | "Tests"
20 | ],
21 | "in": "vendor"
22 | }
23 | ],
24 | "git-version": "package_version",
25 | "key": ".travis/phar-private.pem",
26 | "main": "bin/handle",
27 | "output": "handle.phar",
28 | "stub": true
29 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gilbitron/handle",
3 | "description": "A static site generator powered by PHP and the command line",
4 | "keywords": ["cli", "static", "site", "generator"],
5 | "license": "MIT",
6 | "repositories": [
7 | {
8 | "type": "git",
9 | "url": "https://github.com/gilbitron/handle"
10 | }
11 | ],
12 | "authors": [
13 | {
14 | "name": "Gilbert Pellegrom",
15 | "email": "gilbert@pellegrom.me"
16 | }
17 | ],
18 | "require": {
19 | "symfony/console": "^3.0",
20 | "windwalker/renderer": "^2.1",
21 | "illuminate/view": "^5.2",
22 | "erusev/parsedown": "^1.6",
23 | "symfony/yaml": "^3.0",
24 | "padraic/phar-updater": "^1.0",
25 | "jasonlewis/resource-watcher": "^1.2"
26 | },
27 | "require-dev": {
28 | "phpunit/phpunit": "^5.2"
29 | },
30 | "autoload": {
31 | "psr-4": {
32 | "Handle\\": "src/",
33 | "Handle\\Tests\\": "tests/"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/init-structure/_cache/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gilbitron/Handle/8c5d553729f2b4b82622543e25c9804f902b9cab/init-structure/_cache/.gitkeep
--------------------------------------------------------------------------------
/init-structure/_content/about.md:
--------------------------------------------------------------------------------
1 | Title: About
2 | ---
3 | # About
4 |
5 | This is a test page.
6 |
--------------------------------------------------------------------------------
/init-structure/_content/index.md:
--------------------------------------------------------------------------------
1 | Title: Welcome to Handle
2 | ---
3 | # Welcome to Handle
4 |
5 | Welcome to your Handle site! This was generated by Handle.
6 |
--------------------------------------------------------------------------------
/init-structure/_themes/default/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('layout')
2 |
3 | @section('content')
4 | {!! $content !!}
5 | @endsection
--------------------------------------------------------------------------------
/init-structure/_themes/default/layout.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $title ? $title . ' - ' : '' }}{{ $config['site_title'] }}
4 |
5 |
6 |
7 |
8 | @yield('content')
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/init-structure/config.yml:
--------------------------------------------------------------------------------
1 | site_title: Handle
2 | theme: default
3 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Commands/BuildCommand.php:
--------------------------------------------------------------------------------
1 | 'Handle',
18 | 'theme' => 'default',
19 | 'cache_path' => '_cache',
20 | 'content_path' => '_content',
21 | 'themes_path' => '_themes',
22 | 'build_path' => '',
23 | ];
24 |
25 | protected $metaDefaults = [
26 | 'title' => '',
27 | 'template' => 'index',
28 | ];
29 |
30 | private $fileManifest = [];
31 |
32 | protected function configure()
33 | {
34 | $this->setName('build')
35 | ->setDescription('Build your Handle site by generating the static output')
36 | ->addOption('path', null, InputOption::VALUE_REQUIRED, 'Path to your Handle site')
37 | ->addOption('watch', null, InputOption::VALUE_NONE, 'Constantly watch for changes to your Handle site and build when a change is detected');
38 | }
39 |
40 | protected function execute(InputInterface $input, OutputInterface $output)
41 | {
42 | $path = $input->getOption('path');
43 | if (!$path || $path == '.') {
44 | $path = getcwd();
45 | }
46 |
47 | $watch = $input->getOption('watch');
48 |
49 | try {
50 | $config = $this->getConfig($path);
51 | $config['cache_path'] = $this->prepPath($config['cache_path'], $path . DIRECTORY_SEPARATOR .
52 | '_cache', 'cache');
53 | $config['content_path'] = $this->prepPath($config['content_path'], $path . DIRECTORY_SEPARATOR .
54 | '_content', 'content');
55 | $config['themes_path'] = $this->prepPath($config['themes_path'], $path . DIRECTORY_SEPARATOR .
56 | '_themes', 'themes');
57 | $config['build_path'] = $this->prepPath($config['build_path'], $path, 'build');
58 |
59 | $themePath = $config['themes_path'] . DIRECTORY_SEPARATOR . $config['theme'];
60 | if (!is_dir($themePath)) {
61 | throw new \Exception('The theme "' . $themePath . '" does not exist');
62 | }
63 |
64 | if ($watch) {
65 | $this->runWatch($config, $input, $output);
66 | } else {
67 | $this->runBuild($config, $input, $output);
68 | }
69 | } catch (\Exception $e) {
70 | $output->writeln('' . $e->getMessage() . '');
71 | if ($output->isDebug()) {
72 | $output->writeln('' . $e->getTraceAsString() . '');
73 | }
74 | }
75 | }
76 |
77 | /**
78 | * Run the build command
79 | *
80 | * @param array $config
81 | * @param InputInterface $input
82 | * @param OutputInterface $output
83 | * @param bool $isWatch
84 | * @param string|null $singleContentFile
85 | */
86 | protected function runBuild($config, InputInterface $input, OutputInterface $output, $isWatch = false, $singleContentFile = null)
87 | {
88 | $renderer = $this->getRenderer($config['themes_path'] . DIRECTORY_SEPARATOR .
89 | $config['theme'], $config['cache_path']);
90 |
91 | if ($singleContentFile) {
92 | $output->writeln('Cleaning...');
93 | $this->cleanBuiltContent(str_replace($config['content_path'], $config['build_path'], dirname($singleContentFile)), $output);
94 |
95 | $contentFiles = [$singleContentFile];
96 | } else {
97 | $output->writeln('Cleaning...');
98 | $this->cleanBuiltContent($config['build_path'], $output);
99 |
100 | $contentFiles = $this->getContentFiles($config['content_path']);
101 | }
102 |
103 | $output->writeln('Building...');
104 | foreach ($contentFiles as $contentFile) {
105 | if (!file_exists($contentFile)) {
106 | $output->writeln('Content file does not exist: ' . $contentFile . '');
107 | continue;
108 | }
109 |
110 | $content = file_get_contents($contentFile);
111 | $meta = $this->parseMeta($content);
112 | $parsedContent = $this->parseContent($content);
113 |
114 | $filename = basename($contentFile, '.md');
115 | if ($filename == 'index') {
116 | $filepath = str_replace($config['content_path'], $config['build_path'], dirname($contentFile));
117 | $fullFilepath = $filepath . DIRECTORY_SEPARATOR . $filename . '.html';
118 | } else {
119 | $filepath = str_replace($config['content_path'], $config['build_path'], dirname($contentFile)) .
120 | DIRECTORY_SEPARATOR . $filename;
121 | $fullFilepath = $filepath . DIRECTORY_SEPARATOR . 'index.html';
122 | }
123 |
124 | if (!is_dir($filepath)) {
125 | mkdir($filepath, 0777, true);
126 | }
127 |
128 | $slug = '/' . ltrim(str_replace($config['build_path'], '', $filepath), '/');
129 |
130 | $html = $renderer->render($meta['template'], [
131 | 'config' => $config,
132 | 'title' => $meta['title'],
133 | 'slug' => $slug,
134 | 'content' => $parsedContent,
135 | 'meta' => $meta,
136 | 'build_path' => $config['build_path'],
137 | ]);
138 | file_put_contents($fullFilepath, $html);
139 |
140 | $output->writeln(str_replace($config['build_path'], '', $fullFilepath) . ' generated...');
141 | }
142 |
143 | $output->writeln('Finished building site');
144 |
145 | if ($isWatch) {
146 | $output->writeln('Watching for changes...');
147 | }
148 | }
149 |
150 | /**
151 | * Watch for file changes and trigger the build command if required
152 | *
153 | * @param array $config
154 | * @param InputInterface $input
155 | * @param OutputInterface $output
156 | */
157 | protected function runWatch($config, InputInterface $input, OutputInterface $output)
158 | {
159 | // Run the initial build
160 | $this->runBuild($config, $input, $output, true);
161 |
162 | $files = new \Illuminate\Filesystem\Filesystem;
163 | $tracker = new \JasonLewis\ResourceWatcher\Tracker;
164 | $watcher = new \JasonLewis\ResourceWatcher\Watcher($tracker, $files);
165 |
166 | $contentListener = $watcher->watch($config['content_path']);
167 | $contentListener->anything(function ($event, $resource, $path) use ($config, $input, $output) {
168 | $output->writeln('Content file changed: ' . str_replace($config['build_path'], '', $path));
169 | $this->runBuild($config, $input, $output, true, $path);
170 | });
171 |
172 | $themeListener = $watcher->watch($config['themes_path'] . DIRECTORY_SEPARATOR . $config['theme']);
173 | $themeListener->anything(function ($event, $resource, $path) use ($config, $input, $output) {
174 | $output->writeln('Theme file changed: ' . str_replace($config['build_path'], '', $path));
175 | $this->runBuild($config, $input, $output, true);
176 | });
177 |
178 | $watcher->start();
179 | }
180 |
181 | /**
182 | * Get the site config
183 | *
184 | * @param string $sitePath
185 | * @return array
186 | */
187 | protected function getConfig($sitePath)
188 | {
189 | if (file_exists($sitePath . DIRECTORY_SEPARATOR . 'config.yml')) {
190 | $config = file_get_contents($sitePath . DIRECTORY_SEPARATOR . 'config.yml');
191 |
192 | $yaml = new YamlParser();
193 | $parsedConfig = $yaml->parse($config);
194 |
195 | if (is_array($parsedConfig) && !empty($parsedConfig)) {
196 | $parsedConfig = array_merge($this->configDefaults, $parsedConfig);
197 | } else {
198 | $parsedConfig = $this->configDefaults;
199 | }
200 |
201 | return $parsedConfig;
202 | }
203 |
204 | return $this->configDefaults;
205 | }
206 |
207 | /**
208 | * Get the renderer
209 | *
210 | * @param string $themePath
211 | * @param string $cachePath
212 | * @return AbstractEngineRenderer
213 | */
214 | protected function getRenderer($themePath, $cachePath)
215 | {
216 | return new BladeRenderer([$themePath], ['cache_path' => $cachePath]);
217 | }
218 |
219 | /**
220 | * Clean all previously built files
221 | *
222 | * @param string $buildPath
223 | */
224 | protected function cleanBuiltContent($buildPath, OutputInterface $output)
225 | {
226 | if (!is_dir($buildPath)) {
227 | return;
228 | }
229 |
230 | $rdi = new \RecursiveDirectoryIterator($buildPath, \RecursiveDirectoryIterator::SKIP_DOTS);
231 | $rii = new \RecursiveIteratorIterator($rdi, \RecursiveIteratorIterator::CHILD_FIRST);
232 | foreach ($rii as $fileInfo) {
233 | if ($fileInfo->isFile() && $fileInfo->getExtension() == 'html') {
234 | $output->writeln('Removing file: ' . str_replace($buildPath, '', $fileInfo->getRealPath()) . '...');
235 | unlink($fileInfo->getRealPath());
236 | }
237 | }
238 | }
239 |
240 | /**
241 | * Get all of the valid files from the contents directory
242 | *
243 | * @param string $contentPath
244 | * @return array
245 | */
246 | protected function getContentFiles($contentPath)
247 | {
248 | return $this->getAllFilesFromDir($contentPath, 'md');
249 | }
250 |
251 | /**
252 | * Get all of the template files from the current theme
253 | *
254 | * @param string $currentThemePath
255 | * @return array
256 | */
257 | protected function getThemeFiles($currentThemePath)
258 | {
259 | return $this->getAllFilesFromDir($currentThemePath, 'php');
260 | }
261 |
262 | /**
263 | * Parse the file meta from the content
264 | *
265 | * @param string $content
266 | * @return array
267 | */
268 | protected function parseMeta($content)
269 | {
270 | if (!preg_match('/\-{3,}/m', $content)) {
271 | return $this->metaDefaults;
272 | }
273 |
274 | $parts = preg_split('/\-{3,}/m', $content, 2);
275 | $meta = isset($parts[0]) ? $parts[0] : '';
276 |
277 | $yaml = new YamlParser();
278 | $parsedMeta = $yaml->parse($meta);
279 |
280 | if (is_array($parsedMeta)) {
281 | $parsedMeta = array_merge($this->metaDefaults, $parsedMeta);
282 | } else {
283 | $parsedMeta = $this->metaDefaults;
284 | }
285 |
286 | $parsedMeta = array_change_key_case($parsedMeta, CASE_LOWER);
287 |
288 | return $parsedMeta;
289 | }
290 |
291 | /**
292 | * Parse the content
293 | *
294 | * @param string $content
295 | * @return string
296 | */
297 | protected function parseContent($content)
298 | {
299 | $parts = preg_split('/\-{3,}/m', $content, 2);
300 | $contents = isset($parts[1]) ? $parts[1] : '';
301 |
302 | if ($contents) {
303 | return Parsedown::instance()->text(trim($contents));
304 | }
305 |
306 | return $content;
307 | }
308 |
309 | /**
310 | * Get all files (including subfiles) from a directory
311 | *
312 | * @param string $directory
313 | * @param string $extension
314 | * @return array
315 | */
316 | private function getAllFilesFromDir($directory, $extension = '')
317 | {
318 | if (!is_dir($directory)) {
319 | return [];
320 | }
321 |
322 | $files = [];
323 | $rdi = new \RecursiveDirectoryIterator($directory);
324 | $rii = new \RecursiveIteratorIterator($rdi);
325 | foreach ($rii as $fileInfo) {
326 | if ($fileInfo->isFile()) {
327 | if ($extension) {
328 | if ($fileInfo->getExtension() == $extension) {
329 | $files[] = $fileInfo->getPathname();
330 | }
331 | } else {
332 | $files[] = $fileInfo->getPathname();
333 | }
334 | }
335 | }
336 |
337 | return $files;
338 | }
339 |
340 | /**
341 | * Prep a path to make sure it exists and is absolute
342 | *
343 | * @param string $path
344 | * @param string $default
345 | * @param string $name
346 | * @return string
347 | * @throws \Exception
348 | */
349 | private function prepPath($path, $default, $name)
350 | {
351 | if (!$path) {
352 | $path = $default;
353 | }
354 |
355 | $path = realpath($path);
356 | if (!$path) {
357 | $path = $default;
358 | }
359 | if (!realpath($path)) {
360 | throw new \Exception('The path to the ' . $name . ' directory does not exist: ' . $path);
361 | }
362 |
363 | return $path;
364 | }
365 | }
--------------------------------------------------------------------------------
/src/Commands/InitCommand.php:
--------------------------------------------------------------------------------
1 | setName('init')
16 | ->setDescription('Create the Handle site structure in the given directory')
17 | ->addArgument('path', InputArgument::OPTIONAL, 'Path to create your Handle site in');
18 | }
19 |
20 | protected function execute(InputInterface $input, OutputInterface $output)
21 | {
22 | $path = $input->getArgument('path');
23 | if (!$path || $path == '.') {
24 | $path = getcwd();
25 | }
26 |
27 | if (!is_dir($path)) {
28 | if (!mkdir($path, 0777, true)) {
29 | $output->writeln('Error creating root folder ' . $path . '');
30 | }
31 | }
32 |
33 | try {
34 | $this->copyStructure($path, $output);
35 | $output->writeln('Site created');
36 | } catch (\Exception $e) {
37 | $output->writeln('' . $e->getMessage() . '');
38 | }
39 | }
40 |
41 | /**
42 | * Copy the site structure to the given path
43 | *
44 | * @param string $sitePath
45 | * @param OutputInterface $output
46 | */
47 | protected function copyStructure($sitePath, OutputInterface $output)
48 | {
49 | $source = HANDLE_ROOT . DIRECTORY_SEPARATOR . 'init-structure';
50 | $rdi = new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS);
51 | $rii = new \RecursiveIteratorIterator($rdi, \RecursiveIteratorIterator::SELF_FIRST);
52 | foreach ($rii as $fileInfo) {
53 | if ($fileInfo->isDir()) {
54 | if (!is_dir($sitePath . DIRECTORY_SEPARATOR . $rii->getSubPathName())) {
55 | mkdir($sitePath . DIRECTORY_SEPARATOR . $rii->getSubPathName());
56 | $output->writeln('Creating directory: ' . $rii->getSubPathName() . '...');
57 | }
58 | } else {
59 | if (!file_exists($sitePath . DIRECTORY_SEPARATOR . $rii->getSubPathName())) {
60 | copy($fileInfo, $sitePath . DIRECTORY_SEPARATOR . $rii->getSubPathName());
61 | $output->writeln('Creating file: ' . $rii->getSubPathName() . '...');
62 | }
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/Commands/RollbackCommand.php:
--------------------------------------------------------------------------------
1 | setName('rollback')->setDescription('Rollback an update to the Handle CLI');
17 | }
18 |
19 | protected function execute(InputInterface $input, OutputInterface $output)
20 | {
21 | $updater = new Updater();
22 | $updater->getStrategy()->setPharUrl('https://gilbitron.github.io/Handle/handle.phar');
23 | $updater->getStrategy()->setVersionUrl('https://gilbitron.github.io/Handle/handle.phar.version');
24 |
25 | try {
26 | $result = $updater->rollback();
27 | if (!$result) {
28 | $output->writeln('There was an error rolling back the update');
29 | return;
30 | }
31 |
32 | $output->writeln('Rollback successful');
33 | } catch (\Exception $e) {
34 | $output->writeln('' . $e->getMessage() . '');
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/src/Commands/UpdateCommand.php:
--------------------------------------------------------------------------------
1 | setName('update')->setDescription('Update the Handle CLI');
17 | }
18 |
19 | protected function execute(InputInterface $input, OutputInterface $output)
20 | {
21 | $updater = new Updater();
22 | $updater->getStrategy()->setPharUrl('https://gilbitron.github.io/Handle/handle.phar');
23 | $updater->getStrategy()->setVersionUrl('https://gilbitron.github.io/Handle/handle.phar.version');
24 |
25 | try {
26 | $result = $updater->update();
27 | if (!$result) {
28 | $output->writeln('No update available');
29 | return;
30 | }
31 |
32 | $new = $updater->getNewVersion();
33 | $old = $updater->getOldVersion();
34 | $output->writeln(sprintf('Updated from %s to %s', $old, $new));
35 | } catch (\Exception $e) {
36 | $output->writeln('' . $e->getMessage() . '');
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/tests/Commands/BuildCommandTest.php:
--------------------------------------------------------------------------------
1 | command = $this->app->find('build');
15 |
16 | $initCommand = $this->app->find('init');
17 | $commandTester = new CommandTester($initCommand);
18 | $commandTester->execute([
19 | 'command' => $initCommand->getName(),
20 | 'path' => $this->sitePath,
21 | ]);
22 | }
23 |
24 | public function testExecute()
25 | {
26 | $commandTester = new CommandTester($this->command);
27 | $commandTester->execute([
28 | 'command' => $this->command->getName(),
29 | '--path' => $this->sitePath,
30 | ]);
31 |
32 | $this->assertFileExists($this->sitePath . DIRECTORY_SEPARATOR . 'index.html');
33 | $this->assertFileExists($this->sitePath . DIRECTORY_SEPARATOR . 'about' . DIRECTORY_SEPARATOR . 'index.html');
34 | $this->assertContains('Welcome to your Handle site!', file_get_contents($this->sitePath . DIRECTORY_SEPARATOR . 'index.html'));
35 | $this->assertContains('This is a test page.', file_get_contents($this->sitePath . DIRECTORY_SEPARATOR . 'about' . DIRECTORY_SEPARATOR . 'index.html'));
36 | }
37 | }
--------------------------------------------------------------------------------
/tests/Commands/InitCommandTest.php:
--------------------------------------------------------------------------------
1 | command = $this->app->find('init');
15 | }
16 |
17 | public function testExecute()
18 | {
19 | $commandTester = new CommandTester($this->command);
20 | $commandTester->execute([
21 | 'command' => $this->command->getName(),
22 | 'path' => $this->sitePath,
23 | ]);
24 |
25 | $this->assertFileExists($this->sitePath . DIRECTORY_SEPARATOR . 'config.yml');
26 | $this->assertFileExists($this->sitePath . DIRECTORY_SEPARATOR . '_content' . DIRECTORY_SEPARATOR . 'index.md');
27 | $this->assertFileExists($this->sitePath . DIRECTORY_SEPARATOR . '_content' . DIRECTORY_SEPARATOR . 'about.md');
28 | $this->assertTrue(is_dir($this->sitePath . DIRECTORY_SEPARATOR . '_cache'));
29 | $this->assertTrue(is_dir($this->sitePath . DIRECTORY_SEPARATOR . '_content'));
30 | $this->assertTrue(is_dir($this->sitePath . DIRECTORY_SEPARATOR . '_themes'));
31 | $this->assertTrue(is_dir($this->sitePath . DIRECTORY_SEPARATOR . '_themes' . DIRECTORY_SEPARATOR . 'default'));
32 | }
33 | }
--------------------------------------------------------------------------------
/tests/HandleTestCase.php:
--------------------------------------------------------------------------------
1 | app = new Application();
24 | $this->app->add(new InitCommand());
25 | $this->app->add(new BuildCommand());
26 |
27 | $this->sitePath = HANDLE_TESTS_ROOT . DIRECTORY_SEPARATOR . 'output' . DIRECTORY_SEPARATOR . 'site';
28 | if (!is_dir($this->sitePath)) {
29 | mkdir($this->sitePath, 0777, true);
30 | }
31 |
32 | $rdi = new \RecursiveDirectoryIterator($this->sitePath, \RecursiveDirectoryIterator::SKIP_DOTS);
33 | $rii = new \RecursiveIteratorIterator($rdi, \RecursiveIteratorIterator::CHILD_FIRST);
34 | foreach ($rii as $fileInfo) {
35 | if ($fileInfo->isDir()) {
36 | rmdir($fileInfo->getPathname());
37 | } else {
38 | unlink($fileInfo->getPathname());
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |