├── .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 | [![Build Status](https://travis-ci.org/gilbitron/Handle.svg?branch=master)](https://travis-ci.org/gilbitron/Handle) 2 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](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 |