├── LICENSE ├── README.md ├── composer.json ├── jigsaw ├── src ├── Bootstrap │ └── HandleExceptions.php ├── Collection │ ├── Collection.php │ ├── CollectionItem.php │ ├── CollectionPaginator.php │ └── CollectionRemoteItem.php ├── CollectionItemHandlers │ ├── BladeCollectionItemHandler.php │ └── MarkdownCollectionItemHandler.php ├── Console │ ├── BuildCommand.php │ ├── Command.php │ ├── ConsoleOutput.php │ ├── ConsoleSession.php │ ├── InitCommand.php │ ├── NullProgressBar.php │ ├── ProgressBar.php │ └── ServeCommand.php ├── Container.php ├── Events │ └── EventBus.php ├── Exceptions │ ├── DeprecationException.php │ └── Handler.php ├── File │ ├── BladeDirectivesFile.php │ ├── ConfigFile.php │ ├── CopyFile.php │ ├── Filesystem.php │ ├── InputFile.php │ ├── OutputFile.php │ └── TemporaryFilesystem.php ├── Handlers │ ├── BladeHandler.php │ ├── CollectionItemHandler.php │ ├── DefaultHandler.php │ ├── IgnoredHandler.php │ ├── MarkdownHandler.php │ └── PaginatedPageHandler.php ├── IterableObject.php ├── IterableObjectWithDefault.php ├── Jigsaw.php ├── Loaders │ ├── CollectionDataLoader.php │ ├── CollectionRemoteItemLoader.php │ └── DataLoader.php ├── PageData.php ├── PageVariable.php ├── Parsers │ ├── CommonMarkParser.php │ ├── FrontMatterParser.php │ ├── JigsawMarkdownParser.php │ ├── MarkdownParser.php │ └── MarkdownParserContract.php ├── PathResolvers │ ├── BasicOutputPathResolver.php │ ├── CollectionPathResolver.php │ └── PrettyOutputPathResolver.php ├── Providers │ ├── BootstrapFileServiceProvider.php │ ├── CollectionServiceProvider.php │ ├── CompatibilityServiceProvider.php │ ├── EventServiceProvider.php │ ├── ExceptionServiceProvider.php │ ├── FilesystemServiceProvider.php │ ├── MarkdownServiceProvider.php │ └── ViewServiceProvider.php ├── Scaffold │ ├── BasicScaffoldBuilder.php │ ├── CustomInstaller.php │ ├── DefaultInstaller.php │ ├── InstallerCommandException.php │ ├── PresetPackage.php │ ├── PresetScaffoldBuilder.php │ ├── ProcessRunner.php │ └── ScaffoldBuilder.php ├── SiteBuilder.php ├── SiteData.php ├── Support │ ├── ServiceProvider.php │ └── helpers.php └── View │ ├── BladeCompiler.php │ ├── BladeMarkdownEngine.php │ ├── ComponentTagCompiler.php │ ├── DynamicComponent.php │ ├── MarkdownEngine.php │ └── ViewRenderer.php └── stubs └── site ├── .gitignore ├── bootstrap.php ├── config.php ├── config.production.php ├── package.json ├── source ├── _assets │ ├── css │ │ └── main.css │ └── js │ │ └── main.js ├── _layouts │ └── main.blade.php ├── assets │ ├── build │ │ ├── css │ │ │ └── main.css │ │ ├── js │ │ │ └── main.js │ │ └── mix-manifest.json │ └── images │ │ └── jigsaw.png └── index.blade.php ├── tailwind.config.js └── webpack.mix.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Jigsaw logo](https://raw.githubusercontent.com/tighten/jigsaw/main/jigsaw-banner.png) 2 | 3 | [![Test Suite](https://github.com/tighten/jigsaw/actions/workflows/test.yml/badge.svg)](https://github.com/tighten/jigsaw/actions/workflows/test.yml) 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/tightenco/jigsaw.svg?style=flat)](https://packagist.org/packages/tightenco/jigsaw) 5 | [![Downloads on Packagist](https://img.shields.io/packagist/dt/tightenco/jigsaw.svg?style=flat)](https://packagist.org/packages/tightenco/jigsaw) 6 | 7 | Simple static sites with Laravel's [Blade](https://laravel.com/docs/blade). 8 | 9 | For documentation, visit https://jigsaw.tighten.com/docs/installation/ 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tightenco/jigsaw", 3 | "description": "Simple static sites with Laravel's Blade.", 4 | "keywords": [ 5 | "blade", 6 | "laravel", 7 | "static", 8 | "site", 9 | "generator" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Adam Wathan", 15 | "email": "adam.wathan@gmail.com" 16 | }, 17 | { 18 | "name": "Keith Damiani", 19 | "email": "keith@tighten.com" 20 | }, 21 | { 22 | "name": "Jacob Baker-Kretzmar", 23 | "email": "jacob@tighten.com" 24 | } 25 | ], 26 | "require": { 27 | "php": "^8.2", 28 | "illuminate/collections": "^11.0 || ^12.0", 29 | "illuminate/console": "^11.0 || ^12.0", 30 | "illuminate/container": "^11.0 || ^12.0", 31 | "illuminate/filesystem": "^11.0 || ^12.0", 32 | "illuminate/support": "^11.0 || ^12.0", 33 | "illuminate/view": "^11.0 || ^12.0", 34 | "league/commonmark": "^2.4", 35 | "michelf/php-markdown": "^2.0", 36 | "mnapoli/front-yaml": "^2.0", 37 | "nunomaduro/collision": "^8.1", 38 | "spatie/laravel-ignition": "^2.4", 39 | "symfony/console": "^6.0 || ^7.0", 40 | "symfony/error-handler": "^6.0 || ^7.0", 41 | "symfony/finder": "^6.0 || ^7.0", 42 | "symfony/process": "^6.0 || ^7.0", 43 | "symfony/var-dumper": "^6.0 || ^7.0", 44 | "symfony/yaml": "^6.0 || ^7.0", 45 | "vlucas/phpdotenv": "^5.6" 46 | }, 47 | "require-dev": { 48 | "laravel/pint": "^1.16", 49 | "mockery/mockery": "^1.6", 50 | "phpunit/phpunit": "^11.0.6" 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "TightenCo\\Jigsaw\\": "src/" 55 | }, 56 | "files": [ 57 | "src/Support/helpers.php" 58 | ] 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "Tests\\": "tests/" 63 | } 64 | }, 65 | "bin": [ 66 | "jigsaw" 67 | ], 68 | "config": { 69 | "sort-packages": true, 70 | "optimize-autoloader": true 71 | }, 72 | "minimum-stability": "dev", 73 | "prefer-stable": true 74 | } 75 | -------------------------------------------------------------------------------- /jigsaw: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | singleton( 17 | Illuminate\Contracts\Debug\ExceptionHandler::class, 18 | TightenCo\Jigsaw\Exceptions\Handler::class, 19 | ); 20 | 21 | $app->bootstrapWith([ 22 | TightenCo\Jigsaw\Bootstrap\HandleExceptions::class, 23 | ]); 24 | 25 | $application = new Symfony\Component\Console\Application('Jigsaw', '1.7.6'); 26 | $application->add($app[TightenCo\Jigsaw\Console\InitCommand::class]); 27 | $application->add(new TightenCo\Jigsaw\Console\BuildCommand($app)); 28 | $application->add(new TightenCo\Jigsaw\Console\ServeCommand($app)); 29 | $application->setCatchExceptions(false); 30 | 31 | TightenCo\Jigsaw\Jigsaw::addUserCommands($application, $app); 32 | 33 | $application->run(); 34 | -------------------------------------------------------------------------------- /src/Bootstrap/HandleExceptions.php: -------------------------------------------------------------------------------- 1 | null); 36 | restore_exception_handler(); 37 | 38 | if ($previousHandler === null) { 39 | break; 40 | } 41 | 42 | restore_exception_handler(); 43 | } 44 | 45 | while (true) { 46 | $previousHandler = set_error_handler(static fn () => null); 47 | restore_error_handler(); 48 | 49 | if ($previousHandler === null) { 50 | break; 51 | } 52 | 53 | restore_error_handler(); 54 | } 55 | 56 | if (class_exists(ErrorHandler::class)) { 57 | $instance = ErrorHandler::instance(); 58 | 59 | if ((fn () => $this->enabled ?? false)->call($instance)) { 60 | $instance->disable(); 61 | $instance->enable(); 62 | } 63 | } 64 | } 65 | 66 | public function bootstrap(Container $app): void 67 | { 68 | self::$reservedMemory = str_repeat('x', 32768); 69 | 70 | static::$app = $app; 71 | 72 | error_reporting(-1); 73 | 74 | set_error_handler($this->forwardTo('handleError')); 75 | set_exception_handler($this->forwardTo('handleException')); 76 | register_shutdown_function($this->forwardTo('handleShutdown')); 77 | 78 | /* @internal The '__testing' binding is for Jigsaw development only and may be removed. */ 79 | if (! $app->has('__testing') || ! $app['__testing']) { 80 | ini_set('display_errors', 'Off'); 81 | } 82 | } 83 | 84 | /** 85 | * Report PHP deprecations, or convert PHP errors to ErrorException instances. 86 | * 87 | * @param int $level 88 | * @param string $message 89 | * @param string $file 90 | * @param int $line 91 | * @param array $context 92 | * 93 | * @throws ErrorException 94 | */ 95 | private function handleError($level, $message, $file = '', $line = 0, $context = []): void 96 | { 97 | if (in_array($level, [E_DEPRECATED, E_USER_DEPRECATED])) { 98 | $this->handleDeprecation(new DeprecationException($message, 0, $level, $file, $line)); 99 | 100 | return; 101 | } 102 | 103 | if (error_reporting() & $level) { 104 | throw new ErrorException($message, 0, $level, $file, $line); 105 | } 106 | } 107 | 108 | /** 109 | * Handle a deprecation. 110 | * 111 | * @throws \TightenCo\Jigsaw\Exceptions\DeprecationException 112 | */ 113 | private function handleDeprecation(Throwable $e): void 114 | { 115 | /* @internal The '__testing' binding is for Jigsaw development only and may be removed. */ 116 | if (static::$app->has('__testing') && static::$app['__testing']) { 117 | throw $e; 118 | } 119 | 120 | try { 121 | static::$app->make(ExceptionHandler::class)->report($e); 122 | } catch (Exception $e) { 123 | // 124 | } 125 | 126 | static::$app->make(ExceptionHandler::class)->renderForConsole(new ConsoleOutput, $e); 127 | } 128 | 129 | /** 130 | * Handle an uncaught exception from the application. 131 | * 132 | * Note: Most exceptions can be handled in a try / catch block higher 133 | * in the app, but fatal error exceptions must be handled 134 | * differently since they are not normal exceptions. 135 | */ 136 | private function handleException(Throwable $e): void 137 | { 138 | self::$reservedMemory = null; 139 | 140 | try { 141 | static::$app->make(ExceptionHandler::class)->report($e); 142 | } catch (Exception $e) { 143 | // 144 | } 145 | 146 | static::$app->make(ExceptionHandler::class)->renderForConsole(new ConsoleOutput, $e); 147 | } 148 | 149 | /** 150 | * Handle the PHP shutdown event. 151 | */ 152 | private function handleShutdown(): void 153 | { 154 | self::$reservedMemory = null; 155 | 156 | if ( 157 | ! is_null($error = error_get_last()) 158 | && in_array($error['type'], [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE]) 159 | ) { 160 | $this->handleException(new FatalError($error['message'], 0, $error, 0)); 161 | } 162 | } 163 | 164 | /** 165 | * Forward a method call to the given method on this class if an application instance exists. 166 | */ 167 | private function forwardTo(string $method): callable 168 | { 169 | return fn (...$arguments) => static::$app ? $this->{$method}(...$arguments) : false; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Collection/Collection.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 20 | $collection->name = $name; 21 | 22 | return $collection; 23 | } 24 | 25 | public function loadItems(BaseCollection $items) 26 | { 27 | $sortedItems = $this 28 | ->defaultSort($items) 29 | ->map($this->getMap()) 30 | ->filter($this->getFilter()) 31 | ->keyBy(function ($item) { 32 | return $item->getFilename(); 33 | }); 34 | 35 | return $this->updateItems($this->addAdjacentItems($sortedItems)); 36 | } 37 | 38 | public function updateItems(BaseCollection $items) 39 | { 40 | $this->items = $this->getArrayableItems($items); 41 | 42 | return $this; 43 | } 44 | 45 | private function addAdjacentItems(BaseCollection $items) 46 | { 47 | $count = $items->count(); 48 | $adjacentItems = $items->map(function ($item) { 49 | return $item->getFilename(); 50 | }); 51 | $previousItems = $adjacentItems->prepend(null)->take($count); 52 | $nextItems = $adjacentItems->push(null)->take(-$count); 53 | 54 | return $items->each(function ($item) use ($previousItems, $nextItems) { 55 | $item->_meta->put('previousItem', $previousItems->shift())->put('nextItem', $nextItems->shift()); 56 | }); 57 | } 58 | 59 | private function getFilter() 60 | { 61 | $filter = Arr::get($this->settings, 'filter'); 62 | 63 | if ($filter) { 64 | return $filter; 65 | } 66 | 67 | return function ($item) { 68 | return true; 69 | }; 70 | } 71 | 72 | private function getMap() 73 | { 74 | $map = Arr::get($this->settings, 'map'); 75 | 76 | if ($map) { 77 | return $map; 78 | } 79 | 80 | return function ($item) { 81 | return $item; 82 | }; 83 | } 84 | 85 | private function defaultSort($items) 86 | { 87 | $sortSettings = collect(Arr::get($this->settings, 'sort'))->map(function ($setting) { 88 | return [ 89 | 'key' => ltrim($setting, '-+'), 90 | 'direction' => $setting[0] === '-' ? -1 : 1, 91 | ]; 92 | }); 93 | 94 | if (! $sortSettings->count()) { 95 | $sortSettings = [['key' => 'filename', 'direction' => 1]]; 96 | } 97 | 98 | return $items->sort(function ($item_1, $item_2) use ($sortSettings) { 99 | return $this->compareItems($item_1, $item_2, $sortSettings); 100 | }); 101 | } 102 | 103 | private function compareItems($item_1, $item_2, $sortSettings) 104 | { 105 | foreach ($sortSettings as $setting) { 106 | $value_1 = $this->getValueForSorting($item_1, Arr::get($setting, 'key')); 107 | $value_2 = $this->getValueForSorting($item_2, Arr::get($setting, 'key')); 108 | 109 | if ($value_1 > $value_2) { 110 | return $setting['direction']; 111 | } elseif ($value_1 < $value_2) { 112 | return -$setting['direction']; 113 | } 114 | } 115 | } 116 | 117 | private function getValueForSorting($item, $key) 118 | { 119 | return strtolower($item->$key instanceof Closure ? $item->$key($item) : $item->get($key) ?? $item->_meta->get($key) ?? ''); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Collection/CollectionItem.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 17 | 18 | return $item; 19 | } 20 | 21 | public static function fromItem(CollectionItem $item) 22 | { 23 | $newItem = new static($item); 24 | $newItem->collection = $item->collection; 25 | 26 | return $newItem; 27 | } 28 | 29 | public function getNext() 30 | { 31 | return $this->_meta->nextItem ? $this->collection->get($this->_meta->nextItem) : null; 32 | } 33 | 34 | public function getPrevious() 35 | { 36 | return $this->_meta->previousItem ? $this->collection->get($this->_meta->previousItem) : null; 37 | } 38 | 39 | public function getFirst() 40 | { 41 | return $this->collection->first(); 42 | } 43 | 44 | public function getLast() 45 | { 46 | return $this->collection->last(); 47 | } 48 | 49 | public function setContent($content) 50 | { 51 | $this->_content = $content; 52 | } 53 | 54 | public function getContent() 55 | { 56 | return is_callable($this->_content) ? 57 | call_user_func($this->_content) : 58 | $this->_content; 59 | } 60 | 61 | public function __toString() 62 | { 63 | return (string) $this->getContent(); 64 | } 65 | 66 | protected function missingHelperError($functionName) 67 | { 68 | return 'No function named "' . $functionName . '" for the collection "' . $this->_meta->collectionName . '" was found in the file "config.php".'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Collection/CollectionPaginator.php: -------------------------------------------------------------------------------- 1 | outputPathResolver = $outputPathResolver; 16 | } 17 | 18 | public function paginate($file, $items, $perPage, $prefix) 19 | { 20 | $chunked = collect($items)->chunk($perPage); 21 | $totalPages = $chunked->count(); 22 | $this->prefix = $prefix; 23 | $numberedPageLinks = $chunked->map(function ($_, $i) use ($file) { 24 | $page = $i + 1; 25 | 26 | return ['number' => $page, 'path' => $this->getPageLink($file, $page)]; 27 | })->pluck('path', 'number'); 28 | 29 | return $chunked->map(function ($items, $i) use ($file, $totalPages, $numberedPageLinks) { 30 | $currentPage = $i + 1; 31 | 32 | return new IterableObject([ 33 | 'items' => $items, 34 | 'previous' => $currentPage > 1 ? $this->getPageLink($file, $currentPage - 1) : null, 35 | 'current' => $this->getPageLink($file, $currentPage), 36 | 'next' => $currentPage < $totalPages ? $this->getPageLink($file, $currentPage + 1) : null, 37 | 'first' => $this->getPageLink($file, 1), 38 | 'last' => $this->getPageLink($file, $totalPages), 39 | 'currentPage' => $currentPage, 40 | 'totalPages' => $totalPages, 41 | 'pages' => $numberedPageLinks, 42 | ]); 43 | }); 44 | } 45 | 46 | private function getPageLink($file, $pageNumber) 47 | { 48 | $link = $this->outputPathResolver->link( 49 | $file->getRelativePath(), 50 | $file->getFilenameWithoutExtension(), 51 | 'html', 52 | $pageNumber, 53 | $this->prefix, 54 | ); 55 | 56 | return $link !== '/' ? rightTrimPath($link) : $link; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Collection/CollectionRemoteItem.php: -------------------------------------------------------------------------------- 1 | item = $item; 19 | $this->index = $index; 20 | $this->collectionName = $collectionName; 21 | } 22 | 23 | public function getContent() 24 | { 25 | return is_array($this->item) ? 26 | $this->getHeader() . Arr::get($this->item, 'content') : 27 | $this->item; 28 | } 29 | 30 | public function getFilename() 31 | { 32 | $default = is_int($this->index) 33 | ? $this->collectionName . '-' . ($this->index + 1) 34 | : $this->index; 35 | 36 | return Arr::get($this->item, 'filename', $default) . '.blade.md'; 37 | } 38 | 39 | protected function getHeader() 40 | { 41 | $variables = collect($this->item)->except('content')->toArray(); 42 | 43 | return count($variables) ? "---\n" . Yaml::dump($variables) . "---\n" : null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CollectionItemHandlers/BladeCollectionItemHandler.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 16 | } 17 | 18 | public function shouldHandle($file) 19 | { 20 | return Str::contains($file->getFilename(), '.blade.'); 21 | } 22 | 23 | public function getItemVariables($file) 24 | { 25 | $content = $file->getContents(); 26 | $frontMatter = $this->parser->getFrontMatter($content); 27 | $extendsFromBladeContent = $this->parser->getExtendsFromBladeContent($content); 28 | 29 | return array_merge( 30 | $frontMatter, 31 | ['extends' => $extendsFromBladeContent ?: Arr::get($frontMatter, 'extends')], 32 | ); 33 | } 34 | 35 | public function getItemContent($file) {} 36 | } 37 | -------------------------------------------------------------------------------- /src/CollectionItemHandlers/MarkdownCollectionItemHandler.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 14 | } 15 | 16 | public function shouldHandle($file) 17 | { 18 | return in_array($file->getExtension(), ['markdown', 'md', 'mdown']); 19 | } 20 | 21 | public function getItemVariables($file) 22 | { 23 | return $this->parser->parse($file->getContents())->frontMatter; 24 | } 25 | 26 | public function getItemContent($file) 27 | { 28 | return function () use ($file) { 29 | return $this->parser->parseMarkdown($file->getContents()); 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Console/BuildCommand.php: -------------------------------------------------------------------------------- 1 | app = $app; 26 | $this->consoleOutput = $app->consoleOutput; 27 | parent::__construct(); 28 | } 29 | 30 | protected function configure() 31 | { 32 | $this->setName('build') 33 | ->setDescription('Build your site.') 34 | ->addArgument('env', InputArgument::OPTIONAL, 'What environment should we use to build?', 'local') 35 | ->addOption('pretty', null, InputOption::VALUE_REQUIRED, 'Should the site use pretty URLs?', 'true') 36 | ->addOption('cache', 'c', InputOption::VALUE_OPTIONAL, 'Should a cache be used when building the site?', 'false'); 37 | } 38 | 39 | protected function fire() 40 | { 41 | $startTime = microtime(true); 42 | $env = $this->input->getArgument('env'); 43 | $this->includeEnvironmentConfig($env); 44 | $this->updateBuildPaths($env); 45 | $cacheExists = $this->app[TemporaryFilesystem::class]->hasTempDirectory(); 46 | 47 | if ($this->input->getOption('pretty') === 'true' && $this->app->config->get('pretty') !== false) { 48 | $this->app->instance('outputPathResolver', new PrettyOutputPathResolver); 49 | } 50 | 51 | if ($this->input->getOption('quiet')) { 52 | $verbosity = OutputInterface::VERBOSITY_QUIET; 53 | } elseif ($this->input->getOption('verbose')) { 54 | $verbosity = OutputInterface::VERBOSITY_VERBOSE; 55 | } else { 56 | $verbosity = OutputInterface::VERBOSITY_NORMAL; 57 | } 58 | 59 | $this->consoleOutput->setup($verbosity); 60 | $this->consoleOutput->writeIntro($env, $this->useCache(), $cacheExists); 61 | 62 | if ($this->confirmDestination()) { 63 | try { 64 | $this->app->make(Jigsaw::class)->build($env, $this->useCache()); 65 | } catch (Throwable $e) { 66 | $this->app->make(ExceptionHandler::class)->report($e); 67 | $this->app->make(ExceptionHandler::class)->renderForConsole($this->consoleOutput, $e); 68 | 69 | return static::FAILURE; 70 | } 71 | 72 | $this->consoleOutput 73 | ->writeTime(round(microtime(true) - $startTime, 2), $this->useCache(), $cacheExists) 74 | ->writeConclusion(); 75 | } 76 | } 77 | 78 | private function useCache() 79 | { 80 | return $this->input->getOption('cache') !== 'false' || $this->app->config->get('cache'); 81 | } 82 | 83 | private function includeEnvironmentConfig($env) 84 | { 85 | $environmentConfigPath = $this->getAbsolutePath("config.{$env}.php"); 86 | $environmentConfig = (new ConfigFile($environmentConfigPath))->config; 87 | 88 | $baseConfig = $this->app->config; 89 | 90 | $this->app->config = collect($baseConfig) 91 | ->merge(collect($environmentConfig)) 92 | ->filter(function ($item) { 93 | return $item !== null; 94 | }); 95 | 96 | if ($this->app->config->get('merge_collection_configs')) { 97 | $this->app->config->put('collections', $this->app->config->get('collections')->map( 98 | function ($envConfig, $key) use ($baseConfig) { 99 | return array_merge($baseConfig->get('collections')->get($key), $envConfig); 100 | }, 101 | )); 102 | } 103 | } 104 | 105 | private function updateBuildPaths($env) 106 | { 107 | $this->app->buildPath = [ 108 | 'source' => $this->getBuildPath('source', $env), 109 | 'views' => $this->getBuildPath('views', $env) ?: $this->getBuildPath('source', $env), 110 | 'destination' => $this->getBuildPath('destination', $env), 111 | ]; 112 | } 113 | 114 | private function getBuildPath($pathType, $env) 115 | { 116 | $customPath = Arr::get($this->app->config, 'build.' . $pathType); 117 | $buildPath = $customPath 118 | ? $this->getAbsolutePath($customPath) 119 | : Arr::get($this->app->buildPath, $pathType); 120 | 121 | return str_replace('{env}', $env, $buildPath ?? ''); 122 | } 123 | 124 | private function getAbsolutePath($path) 125 | { 126 | return $this->app->cwd . '/' . trimPath($path); 127 | } 128 | 129 | private function confirmDestination() 130 | { 131 | if (! $this->input->getOption('quiet')) { 132 | $customPath = Arr::get($this->app->config, 'build.destination'); 133 | 134 | if ($customPath && strpos($customPath, 'build_') !== 0 && file_exists($customPath)) { 135 | return $this->console->confirm('Overwrite "' . $this->app->buildPath['destination'] . '"? '); 136 | } 137 | } 138 | 139 | return true; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Console/Command.php: -------------------------------------------------------------------------------- 1 | input = $input; 20 | $this->output = $output; 21 | $this->console = new ConsoleSession( 22 | $this->input, 23 | $this->output, 24 | $this->getHelper('question'), 25 | ); 26 | 27 | return (int) $this->fire(); 28 | } 29 | 30 | abstract protected function fire(); 31 | } 32 | -------------------------------------------------------------------------------- /src/Console/ConsoleOutput.php: -------------------------------------------------------------------------------- 1 | setVerbosity($verbosity); 16 | $this->setupSections(); 17 | $this->setupProgressBars(); 18 | } 19 | 20 | protected function setupSections() 21 | { 22 | $this->sections = collect([ 23 | 'footer' => $this->section(), 24 | 'intro' => $this->section(), 25 | 'message' => $this->section(), 26 | 'progress' => $this->section(), 27 | 'header' => $this->section(), 28 | ])->map(function ($section) { 29 | return $this->section(); 30 | }); 31 | 32 | $this->sections['header']->writeln(''); 33 | $this->sections['footer']->writeln(''); 34 | } 35 | 36 | protected function setupProgressBars() 37 | { 38 | $this->progressBars = [ 39 | 'collections' => $this->getProgressBar('Loading collections...'), 40 | 'build' => $this->getProgressBar('Building files from source...'), 41 | ]; 42 | } 43 | 44 | protected function getProgressBar($message = null) 45 | { 46 | return $this->isVerbose() ? 47 | new ProgressBar($this, $message, $this->sections['progress']) : 48 | new NullProgressBar($this, $message, $this->sections['progress']); 49 | } 50 | 51 | public function progressBar($name) 52 | { 53 | return $this->progressBars[$name]; 54 | } 55 | 56 | public function startProgressBar($name, $steps = null) 57 | { 58 | $this->sections['progress']->clear(); 59 | $progressBar = $this->progressBar($name); 60 | 61 | if ($progressBar->getMessage()) { 62 | $this->sections['message']->overwrite($progressBar->getMessage()); 63 | } 64 | 65 | $progressBar->addSteps($steps)->start(); 66 | } 67 | 68 | public function writeIntro($env, $useCache = false, $cacheExisted = false) 69 | { 70 | if ($useCache) { 71 | if ($cacheExisted) { 72 | $cacheMessage = '(using cache)'; 73 | } else { 74 | $cacheMessage = '(creating cache)'; 75 | } 76 | } else { 77 | $cacheMessage = ''; 78 | } 79 | 80 | $this->sections['intro']->overwrite( 81 | 'Building ' 82 | . $env 83 | . ' site ' 84 | . $cacheMessage 85 | . '', 86 | ); 87 | 88 | return $this; 89 | } 90 | 91 | public function writeWritingFiles() 92 | { 93 | $this->sections['progress']->clear(); 94 | $this->sections['message']->overwrite('Writing files to destination...'); 95 | 96 | return $this; 97 | } 98 | 99 | public function writeTime($time, $useCache = false, $cacheExisted = false) 100 | { 101 | if ($useCache) { 102 | if ($cacheExisted) { 103 | $cacheMessage = '(using cache)'; 104 | } else { 105 | $cacheMessage = '(cache was created)'; 106 | } 107 | } else { 108 | $cacheMessage = ''; 109 | } 110 | 111 | $this->sections['intro']->overwrite( 112 | 'Build time: ' . 113 | $time . 114 | ' seconds ' . 115 | $cacheMessage, 116 | ); 117 | 118 | return $this; 119 | } 120 | 121 | public function writeConclusion() 122 | { 123 | $this->sections['message']->overwrite('Site built successfully!'); 124 | 125 | return $this; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Console/ConsoleSession.php: -------------------------------------------------------------------------------- 1 | input = $input; 23 | $this->output = $output; 24 | $this->question = $question; 25 | } 26 | 27 | public function write($string) 28 | { 29 | $this->output->writeln($string); 30 | 31 | return $this; 32 | } 33 | 34 | public function info($string) 35 | { 36 | return $this->write("{$string}"); 37 | } 38 | 39 | public function error($string) 40 | { 41 | return $this->write("{$string}"); 42 | } 43 | 44 | public function comment($string) 45 | { 46 | return $this->write("{$string}"); 47 | } 48 | 49 | public function line() 50 | { 51 | return $this->write(''); 52 | } 53 | 54 | public function ask($question, $default = null, $choices = null, $errorMessage = '') 55 | { 56 | $defaultPrompt = $default ? '(default ' . $default . ') ' : ''; 57 | 58 | if ($choices) { 59 | $question = new ChoiceQuestion($question . ' ' . $defaultPrompt, $choices, $default ?? false); 60 | $question->setErrorMessage($errorMessage ?: 'Selection "%s" is invalid.'); 61 | } else { 62 | $question = new Question($question . ' ' . $defaultPrompt, $default ?? ''); 63 | } 64 | 65 | return $this->question->ask( 66 | $this->input, 67 | $this->output, 68 | $question, 69 | ); 70 | } 71 | 72 | public function confirm($question, $default = false, $errorMessage = '') 73 | { 74 | $defaultPrompt = $default ? 75 | ' (default y) ' : 76 | ' (default n) '; 77 | 78 | return (bool) $this->question->ask( 79 | $this->input, 80 | $this->output, 81 | new ConfirmationQuestion($question . $defaultPrompt, $default ?? false), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Console/InitCommand.php: -------------------------------------------------------------------------------- 1 | basicScaffold = $basicScaffold; 25 | $this->presetScaffold = $presetScaffold; 26 | $this->files = $files; 27 | $this->setBase(); 28 | parent::__construct(); 29 | } 30 | 31 | public function setBase($cwd = null) 32 | { 33 | $this->base = $cwd ?: getcwd(); 34 | 35 | return $this; 36 | } 37 | 38 | protected function configure() 39 | { 40 | $this->setName('init') 41 | ->setDescription('Scaffold a new Jigsaw project.') 42 | ->addArgument( 43 | 'preset', 44 | InputArgument::OPTIONAL, 45 | 'Which preset should we use to initialize this project?', 46 | ); 47 | } 48 | 49 | protected function fire() 50 | { 51 | $scaffold = $this->getScaffold()->setBase($this->base); 52 | 53 | try { 54 | $scaffold->init($this->input->getArgument('preset')); 55 | } catch (Exception $e) { 56 | $this->console->error($e->getMessage())->line(); 57 | 58 | return; 59 | } 60 | 61 | if ($this->initHasAlreadyBeenRun()) { 62 | $response = $this->askUserWhatToDoWithExistingSite(); 63 | $this->console->line(); 64 | 65 | switch ($response) { 66 | case 'a': 67 | $this->console->comment('Archiving your existing site...'); 68 | $scaffold->archiveExistingSite(); 69 | break; 70 | 71 | case 'd': 72 | if ($this->console->confirm( 73 | 'Are you sure you want to delete your existing site?', 74 | )) { 75 | $this->console->comment('Deleting your existing site...'); 76 | $scaffold->deleteExistingSite(); 77 | break; 78 | } 79 | 80 | // no break 81 | default: 82 | return; 83 | } 84 | } 85 | 86 | try { 87 | $scaffold->setConsole($this->console)->build(); 88 | 89 | $suffix = $scaffold instanceof $this->presetScaffold && $scaffold->package ? 90 | " using the '" . $scaffold->package->shortName . "' preset." : 91 | ' successfully.'; 92 | 93 | $this->console 94 | ->line() 95 | ->info('Your new Jigsaw site was initialized' . $suffix) 96 | ->line(); 97 | } catch (InstallerCommandException $e) { 98 | $this->console 99 | ->line() 100 | ->error("There was an error running the command '" . $e->getMessage() . "'") 101 | ->line(); 102 | } 103 | } 104 | 105 | protected function getScaffold() 106 | { 107 | return $this->input->getArgument('preset') ? 108 | $this->presetScaffold : 109 | $this->basicScaffold; 110 | } 111 | 112 | protected function initHasAlreadyBeenRun() 113 | { 114 | return $this->files->exists($this->base . '/config.php') 115 | || $this->files->exists($this->base . '/source'); 116 | } 117 | 118 | protected function askUserWhatToDoWithExistingSite() 119 | { 120 | $this->console 121 | ->line() 122 | ->comment("It looks like you've already run 'jigsaw init' on this project.") 123 | ->comment('Running it again will overwrite important files.') 124 | ->line(); 125 | 126 | $choices = [ 127 | 'a' => 'archive your existing site, then initialize a new one', 128 | 'd' => 'delete your existing site, then initialize a new one', 129 | 'c' => 'cancel', 130 | ]; 131 | 132 | return $this->console->ask('What would you like to do?', 'a', $choices); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Console/NullProgressBar.php: -------------------------------------------------------------------------------- 1 | consoleOutput = $consoleOutput; 14 | $this->message = $message; 15 | 16 | if ($section) { 17 | $section->writeln(''); 18 | } 19 | } 20 | 21 | public function getMessage() 22 | { 23 | return $this->message ? '' . $this->message . '' : null; 24 | } 25 | 26 | public function start() 27 | { 28 | return $this; 29 | } 30 | 31 | public function addSteps($count) 32 | { 33 | return $this; 34 | } 35 | 36 | public function advance() 37 | { 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/ProgressBar.php: -------------------------------------------------------------------------------- 1 | consoleOutput = $consoleOutput; 18 | $this->progressBar = new SymfonyProgressBar($section ?? $consoleOutput); 19 | $this->message = $message; 20 | } 21 | 22 | public function getMessage() 23 | { 24 | return $this->message ? '' . $this->message . '' : null; 25 | } 26 | 27 | public function start() 28 | { 29 | if ($this->consoleOutput->isVerbose()) { 30 | $this->progressBar->setFormat('normal'); 31 | $this->progressBar->start(); 32 | } 33 | 34 | return $this; 35 | } 36 | 37 | public function addSteps($count) 38 | { 39 | $this->progressBar->setMaxSteps($this->progressBar->getMaxSteps() + $count); 40 | 41 | return $this; 42 | } 43 | 44 | public function advance() 45 | { 46 | $this->progressBar->advance(); 47 | 48 | return $this; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/ServeCommand.php: -------------------------------------------------------------------------------- 1 | app = $app; 18 | parent::__construct(); 19 | } 20 | 21 | protected function configure() 22 | { 23 | $this->setName('serve') 24 | ->setDescription('Serve local site with php built-in server.') 25 | ->addArgument( 26 | 'environment', 27 | InputArgument::OPTIONAL, 28 | 'What environment should we serve?', 29 | 'local', 30 | ) 31 | ->addOption( 32 | 'host', 33 | null, 34 | InputOption::VALUE_OPTIONAL, 35 | 'What hostname or ip address should we use?', 36 | 'localhost', 37 | ) 38 | ->addOption( 39 | 'port', 40 | 'p', 41 | InputOption::VALUE_REQUIRED, 42 | 'What port should we use?', 43 | 8000, 44 | ) 45 | ->addOption( 46 | 'no-build', 47 | null, 48 | InputOption::VALUE_NONE, 49 | 'Skip build before serving?', 50 | ); 51 | } 52 | 53 | protected function fire() 54 | { 55 | $env = $this->input->getArgument('environment'); 56 | $host = $this->input->getOption('host'); 57 | $port = $this->input->getOption('port'); 58 | 59 | if (! $this->input->getOption('no-build')) { 60 | $buildCmd = $this->getApplication()->find('build'); 61 | $buildArgs = new ArrayInput([ 62 | 'env' => $env, 63 | '--quiet' => $this->input->getOption('quiet'), 64 | '--verbose' => $this->input->getOption('verbose'), 65 | ]); 66 | $buildCmd->run($buildArgs, $this->output); 67 | } 68 | 69 | $this->console->info("Server started on http://{$host}:{$port}"); 70 | 71 | passthru("php -S {$host}:{$port} -t " . escapeshellarg($this->getBuildPath($env))); 72 | } 73 | 74 | private function getBuildPath($env) 75 | { 76 | $environmentConfigPath = $this->getAbsolutePath("config.{$env}.php"); 77 | $environmentConfig = file_exists($environmentConfigPath) ? include $environmentConfigPath : []; 78 | 79 | $customBuildPath = Arr::get( 80 | $environmentConfig, 81 | 'build.destination', 82 | Arr::get($this->app->config, 'build.destination'), 83 | ); 84 | 85 | $buildPath = $customBuildPath ? $this->getAbsolutePath($customBuildPath) : $this->app->buildPath['destination']; 86 | 87 | return str_replace('{env}', $env, $buildPath); 88 | } 89 | 90 | private function getAbsolutePath($path) 91 | { 92 | return $this->app->cwd . '/' . trimPath($path); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | path = getcwd(); 30 | 31 | static::setInstance($this); 32 | $this->instance('app', $this); 33 | 34 | $this->registerCoreProviders(); 35 | $this->registerCoreAliases(); 36 | } 37 | 38 | public function bootstrapWith(array $bootstrappers): void 39 | { 40 | $this->bootstrapped = true; 41 | 42 | $this->loadEnvironmentVariables(); 43 | $this->loadConfiguration(); 44 | 45 | foreach ($bootstrappers as $bootstrapper) { 46 | $this->make($bootstrapper)->bootstrap($this); 47 | } 48 | 49 | $this->registerConfiguredProviders(); 50 | $this->boot(); 51 | } 52 | 53 | public function path(string ...$path): string 54 | { 55 | return implode('/', array_filter([$this->path, ...$path])); 56 | } 57 | 58 | public function cachePath(string ...$path): string 59 | { 60 | return $this->path('cache', ...$path); 61 | } 62 | 63 | public function isBooted(): bool 64 | { 65 | return $this->booted; 66 | } 67 | 68 | public function booting(callable $callback): void 69 | { 70 | $this->bootingCallbacks[] = $callback; 71 | } 72 | 73 | public function booted(callable $callback): void 74 | { 75 | $this->bootedCallbacks[] = $callback; 76 | 77 | if ($this->isBooted()) { 78 | $callback($this); 79 | } 80 | } 81 | 82 | private function loadEnvironmentVariables(): void 83 | { 84 | try { 85 | Dotenv::create(Env::getRepository(), $this->path)->safeLoad(); 86 | } catch (InvalidFileException $e) { 87 | $output = (new ConsoleOutput)->getErrorOutput(); 88 | 89 | $output->writeln('The environment file is invalid!'); 90 | $output->writeln($e->getMessage()); 91 | 92 | exit(1); 93 | } 94 | } 95 | 96 | private function loadConfiguration(): void 97 | { 98 | $config = collect(); 99 | 100 | $files = array_filter([ 101 | $this->path('config.php'), 102 | $this->path('helpers.php'), 103 | ], 'file_exists'); 104 | 105 | foreach ($files as $path) { 106 | $config = $config->merge(require $path); 107 | } 108 | 109 | if ($collections = value($config->get('collections'))) { 110 | $config->put('collections', collect($collections)->flatMap( 111 | fn ($value, $key) => is_array($value) ? [$key => $value] : [$value => []], 112 | )); 113 | } 114 | 115 | $this->instance('buildPath', [ 116 | 'source' => $this->path('source'), 117 | 'destination' => $this->path('build_{env}'), 118 | ]); 119 | 120 | $config->put('view.compiled', $this->cachePath()); 121 | 122 | $this->instance('config', $config); 123 | 124 | setlocale(LC_ALL, 'en_US.UTF8'); 125 | } 126 | 127 | private function boot(): void 128 | { 129 | $this->fireAppCallbacks($this->bootingCallbacks); 130 | 131 | array_walk($this->providers, function ($provider) { 132 | if (method_exists($provider, 'boot')) { 133 | $this->call([$provider, 'boot']); 134 | } 135 | }); 136 | 137 | $this->booted = true; 138 | 139 | $this->fireAppCallbacks($this->bootedCallbacks); 140 | } 141 | 142 | /** @param callable[] $callbacks */ 143 | private function fireAppCallbacks(array &$callbacks): void 144 | { 145 | $index = 0; 146 | 147 | while ($index < count($callbacks)) { 148 | $callbacks[$index]($this); 149 | 150 | $index++; 151 | } 152 | } 153 | 154 | private function registerCoreProviders(): void 155 | { 156 | foreach ([ 157 | Providers\EventServiceProvider::class, 158 | ] as $provider) { 159 | ($provider = new $provider($this))->register(); 160 | 161 | $this->providers[] = $provider; 162 | } 163 | } 164 | 165 | private function registerConfiguredProviders(): void 166 | { 167 | foreach ([ 168 | Providers\ExceptionServiceProvider::class, 169 | Providers\FilesystemServiceProvider::class, 170 | Providers\MarkdownServiceProvider::class, 171 | Providers\ViewServiceProvider::class, 172 | Providers\CollectionServiceProvider::class, 173 | Providers\CompatibilityServiceProvider::class, 174 | Providers\BootstrapFileServiceProvider::class, 175 | ] as $provider) { 176 | ($provider = new $provider($this))->register(); 177 | 178 | $this->providers[] = $provider; 179 | } 180 | } 181 | 182 | private function registerCoreAliases(): void 183 | { 184 | foreach ([ 185 | 'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Psr\Container\ContainerInterface::class], 186 | 'view' => [\Illuminate\View\Factory::class, \Illuminate\Contracts\View\Factory::class], 187 | ] as $key => $aliases) { 188 | foreach ($aliases as $alias) { 189 | $this->alias($key, $alias); 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Events/EventBus.php: -------------------------------------------------------------------------------- 1 | $listener) 10 | * @method void afterCollections(\callable|class-string|array $listener) 11 | * @method void afterBuild(\callable|class-string|array $listener) 12 | */ 13 | class EventBus 14 | { 15 | /** @var \Illuminate\Support\Collection */ 16 | public $beforeBuild; 17 | 18 | /** @var \Illuminate\Support\Collection */ 19 | public $afterCollections; 20 | 21 | /** @var \Illuminate\Support\Collection */ 22 | public $afterBuild; 23 | 24 | public function __construct() 25 | { 26 | $this->beforeBuild = collect(); 27 | $this->afterCollections = collect(); 28 | $this->afterBuild = collect(); 29 | } 30 | 31 | public function __call($event, $arguments) 32 | { 33 | if (isset($this->{$event})) { 34 | $this->{$event} = $this->{$event}->merge(Arr::wrap($arguments[0])); 35 | } 36 | } 37 | 38 | public function fire($event, Jigsaw $jigsaw) 39 | { 40 | $this->{$event}->each(function ($task) use ($jigsaw) { 41 | if (is_callable($task)) { 42 | $task($jigsaw); 43 | } else { 44 | (new $task)->handle($jigsaw); 45 | } 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Exceptions/DeprecationException.php: -------------------------------------------------------------------------------- 1 | */ 25 | private array $exceptionMap = []; 26 | 27 | public function report(Throwable $e): void 28 | { 29 | // 30 | } 31 | 32 | public function shouldReport(Throwable $e): bool 33 | { 34 | return true; 35 | } 36 | 37 | public function render($request, Throwable $e): void 38 | { 39 | // 40 | } 41 | 42 | /** 43 | * @param \Symfony\Component\Console\Output\OutputInterface $output 44 | */ 45 | public function renderForConsole($output, Throwable $e): void 46 | { 47 | if ($e instanceof CommandNotFoundException) { 48 | $message = str($e->getMessage())->explode('.')->first(); 49 | 50 | if (! empty($alternatives = $e->getAlternatives())) { 51 | (new Error($output))->render("{$message}. Did you mean one of these?"); 52 | (new BulletList($output))->render($alternatives); 53 | } else { 54 | (new Error($output))->render($message); 55 | } 56 | 57 | return; 58 | } 59 | 60 | if ($e instanceof SymfonyConsoleExceptionInterface) { 61 | (new ConsoleApplication)->renderThrowable($e, $output); 62 | 63 | return; 64 | } 65 | 66 | if ($e instanceof DeprecationException) { 67 | // If the deprecation appears to have come from a compiled Blade view, wrap it in 68 | // a ViewException and map it manually so Ignition will add the uncompiled path 69 | if (preg_match('/cache\/\w+\.php$/', $e->getFile()) === 1) { 70 | $e = $this->mapException( 71 | new ViewException("{$e->getMessage()} (View: )", 0, 1, $e->getFile(), $e->getLine(), $e), 72 | ); 73 | } 74 | 75 | (new Warn($output))->render("{$e->getMessage()} in {$e->getFile()} on line {$e->getLine()}"); 76 | 77 | return; 78 | } 79 | 80 | $e = $this->mapException($e); 81 | 82 | /** @var \NunoMaduro\Collision\Provider $provider */ 83 | $provider = app(Provider::class); 84 | 85 | $handler = $provider->register()->getHandler()->setOutput($output); 86 | $handler->setInspector(new Inspector($e)); 87 | 88 | $handler->handle(); 89 | } 90 | 91 | public function map(Closure|string $from, Closure|string|null $to = null): static 92 | { 93 | if (is_string($to)) { 94 | $to = fn ($exception) => new $to('', 0, $exception); 95 | } 96 | 97 | if (is_callable($from) && is_null($to)) { 98 | $from = $this->firstClosureParameterType($to = $from); 99 | } 100 | 101 | if (! is_string($from) || ! $to instanceof Closure) { 102 | throw new InvalidArgumentException('Invalid exception mapping.'); 103 | } 104 | 105 | $this->exceptionMap[$from] = $to; 106 | 107 | return $this; 108 | } 109 | 110 | protected function mapException(Throwable $e): Throwable 111 | { 112 | if (method_exists($e, 'getInnerException') && ($inner = $e->getInnerException()) instanceof Throwable) { 113 | return $inner; 114 | } 115 | 116 | foreach ($this->exceptionMap as $class => $mapper) { 117 | if ($e instanceof $class) { 118 | return $mapper($e); 119 | } 120 | } 121 | 122 | return $e; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/File/BladeDirectivesFile.php: -------------------------------------------------------------------------------- 1 | bladeCompiler = $bladeCompiler; 16 | $this->directives = file_exists($file_path) ? include $file_path : []; 17 | 18 | if (! is_array($this->directives)) { 19 | $this->directives = []; 20 | } 21 | } 22 | 23 | public function register() 24 | { 25 | collect($this->directives)->each(function ($callback, $directive) { 26 | $this->bladeCompiler->directive($directive, $callback); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/File/ConfigFile.php: -------------------------------------------------------------------------------- 1 | config = collect($config)->merge($helpers); 15 | $this->convertStringCollectionsToArray(); 16 | } 17 | 18 | protected function convertStringCollectionsToArray() 19 | { 20 | $collections = value($this->config->get('collections')); 21 | 22 | if ($collections) { 23 | $this->config->put('collections', collect($collections)->flatMap(function ($value, $key) { 24 | return is_array($value) ? [$key => $value] : [$value => []]; 25 | })); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/File/CopyFile.php: -------------------------------------------------------------------------------- 1 | source = $source; 12 | parent::__construct($file, $path, $name, $extension, null, $data, $page); 13 | } 14 | 15 | public function putContents($destination) 16 | { 17 | return copy($this->source, $destination); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/File/Filesystem.php: -------------------------------------------------------------------------------- 1 | pop(); 22 | $directory_path = rightTrimPath($directory_path->implode('/')); 23 | 24 | if (! $this->isDirectory($directory_path)) { 25 | $this->makeDirectory($directory_path, 0755, true); 26 | } 27 | 28 | $this->put($file_path, $contents); 29 | } 30 | 31 | public function files($directory, $match = [], $ignore = [], $ignore_dotfiles = false) 32 | { 33 | return $directory ? iterator_to_array( 34 | $this->getFinder($directory, $match, $ignore, $ignore_dotfiles)->files(), 35 | false, 36 | ) : []; 37 | } 38 | 39 | public function directories($directory, $match = [], $ignore = [], $ignore_dotfiles = false) 40 | { 41 | return $directory ? iterator_to_array( 42 | $this->getFinder($directory, $match, $ignore, $ignore_dotfiles)->directories(), 43 | false, 44 | ) : []; 45 | } 46 | 47 | public function filesAndDirectories($directory, $match = [], $ignore = [], $ignore_dotfiles = false) 48 | { 49 | return $directory ? iterator_to_array( 50 | $this->getFinder($directory, $match, $ignore, $ignore_dotfiles), 51 | false, 52 | ) : []; 53 | } 54 | 55 | public function isEmptyDirectory($directory, $ignoreDotFiles = false) 56 | { 57 | return $this->exists($directory) ? count($this->files($directory)) == 0 : false; 58 | } 59 | 60 | protected function getFinder($directory, $match = [], $ignore = [], $ignore_dotfiles = false) 61 | { 62 | $finder = Finder::create() 63 | ->in($directory) 64 | ->ignoreDotFiles($ignore_dotfiles) 65 | ->notName('.DS_Store'); 66 | 67 | collect($match)->each(function ($pattern) use ($finder) { 68 | $finder->path($this->getWildcardRegex($pattern)); 69 | }); 70 | 71 | collect($ignore)->each(function ($pattern) use ($finder) { 72 | $finder->notPath($this->getWildcardRegex($pattern)); 73 | }); 74 | 75 | return $finder->sortByName(); 76 | } 77 | 78 | protected function getWildcardRegex($pattern) 79 | { 80 | return '#^' . str_replace('\*', '[^/]+', preg_quote(trim($pattern, '/'))) . '($|/)#'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/File/InputFile.php: -------------------------------------------------------------------------------- 1 | file = $file; 21 | } 22 | 23 | public function setPageData(PageData $pageData) 24 | { 25 | $this->pageData = $pageData->page; 26 | } 27 | 28 | public function getPageData() 29 | { 30 | return $this->pageData; 31 | } 32 | 33 | public function getFileInfo() 34 | { 35 | return $this->file; 36 | } 37 | 38 | public function topLevelDirectory() 39 | { 40 | $parts = explode(DIRECTORY_SEPARATOR, $this->file->getRelativePathName()); 41 | 42 | return count($parts) == 1 ? '' : $parts[0]; 43 | } 44 | 45 | public function getFilenameWithoutExtension() 46 | { 47 | return $this->getBasename('.' . $this->getFullExtension()); 48 | } 49 | 50 | public function getExtension() 51 | { 52 | if (! Str::startsWith($this->getFilename(), '.')) { 53 | return $this->file->getExtension(); 54 | } 55 | } 56 | 57 | public function getFullExtension() 58 | { 59 | return $this->isBladeFile() ? 'blade.' . $this->getExtension() : $this->getExtension(); 60 | } 61 | 62 | public function getExtraBladeExtension() 63 | { 64 | return $this->isBladeFile() && in_array($this->getExtension(), $this->extraBladeExtensions) ? $this->getExtension() : ''; 65 | } 66 | 67 | public function getLastModifiedTime() 68 | { 69 | return $this->file->getMTime(); 70 | } 71 | 72 | public function isBladeFile() 73 | { 74 | return strpos($this->getBasename(), '.blade.' . $this->getExtension()) > 0; 75 | } 76 | 77 | public function __call($method, $args) 78 | { 79 | return $this->file->{$method}(...$args); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/File/OutputFile.php: -------------------------------------------------------------------------------- 1 | setInputFile($inputFile, $data); 28 | $this->path = $path; 29 | $this->name = $name; 30 | $this->extension = $extension; 31 | $this->contents = $contents; 32 | $this->data = $data; 33 | $this->page = $page; 34 | $this->prefix = $prefix; 35 | } 36 | 37 | public function setInputFile(InputFile $inputFile, PageData $data) 38 | { 39 | $this->inputFile = $inputFile; 40 | $this->inputFile->setPageData($data); 41 | } 42 | 43 | public function inputFile() 44 | { 45 | return $this->inputFile; 46 | } 47 | 48 | public function path() 49 | { 50 | return $this->path; 51 | } 52 | 53 | public function name() 54 | { 55 | return $this->name; 56 | } 57 | 58 | public function extension() 59 | { 60 | return $this->extension; 61 | } 62 | 63 | public function contents() 64 | { 65 | return $this->contents; 66 | } 67 | 68 | public function data() 69 | { 70 | return $this->data; 71 | } 72 | 73 | public function page() 74 | { 75 | return $this->page; 76 | } 77 | 78 | public function prefix() 79 | { 80 | return $this->prefix; 81 | } 82 | 83 | public function putContents($destination) 84 | { 85 | return file_put_contents($destination, $this->contents); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/File/TemporaryFilesystem.php: -------------------------------------------------------------------------------- 1 | tempPath = $tempPath; 17 | $this->filesystem = $filesystem ?: new Filesystem; 18 | } 19 | 20 | public function buildTempPath($filename, $extension) 21 | { 22 | return $this->tempPath . DIRECTORY_SEPARATOR . 23 | ($filename ? sha1($filename) : Str::random(32)) . 24 | $extension; 25 | } 26 | 27 | public function get($originalFilename, $extension) 28 | { 29 | $file = new SplFileInfo( 30 | $this->buildTempPath($originalFilename, $extension), 31 | $this->tempPath, 32 | $originalFilename . $extension, 33 | ); 34 | 35 | return $file->isReadable() ? new InputFile($file) : null; 36 | } 37 | 38 | public function put($contents, $filename, $extension) 39 | { 40 | $path = $this->buildTempPath($filename, $extension); 41 | $this->filesystem->put($path, $contents); 42 | 43 | return $path; 44 | } 45 | 46 | public function hasTempDirectory() 47 | { 48 | return $this->filesystem->exists($this->tempPath); 49 | } 50 | 51 | private function delete($path) 52 | { 53 | $this->filesystem->delete($path); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Handlers/BladeHandler.php: -------------------------------------------------------------------------------- 1 | temporaryFilesystem = $temporaryFilesystem; 25 | $this->parser = $parser; 26 | $this->view = $viewRenderer; 27 | } 28 | 29 | public function shouldHandle($file) 30 | { 31 | return Str::contains($file->getFilename(), '.blade.'); 32 | } 33 | 34 | public function handleCollectionItem($file, PageData $pageData) 35 | { 36 | $this->getPageVariables($file); 37 | 38 | return $this->buildOutput($file, $pageData); 39 | } 40 | 41 | public function handle($file, $pageData) 42 | { 43 | $pageData->page->addVariables($this->getPageVariables($file)); 44 | 45 | return $this->buildOutput($file, $pageData); 46 | } 47 | 48 | private function buildOutput($file, $pageData) 49 | { 50 | $extension = strtolower($file->getExtension()); 51 | 52 | return collect([ 53 | new OutputFile( 54 | $file, 55 | $file->getRelativePath(), 56 | $file->getFilenameWithoutExtension(), 57 | $extension == 'php' ? 'html' : $extension, 58 | $this->hasFrontMatter ? 59 | $this->renderWithFrontMatter($file, $pageData) : 60 | $this->render($file->getPathName(), $pageData), 61 | $pageData, 62 | ), 63 | ]); 64 | } 65 | 66 | private function getPageVariables($file) 67 | { 68 | $frontMatter = $this->parseFrontMatter($file); 69 | $this->hasFrontMatter = count($frontMatter) > 0; 70 | 71 | return $frontMatter; 72 | } 73 | 74 | private function parseFrontMatter($file) 75 | { 76 | return $this->parser->getFrontMatter($file->getContents()); 77 | } 78 | 79 | private function render($path, $pageData) 80 | { 81 | return $this->view->render($path, $pageData); 82 | } 83 | 84 | private function renderWithFrontMatter($file, $pageData) 85 | { 86 | $bladeFilePath = $this->temporaryFilesystem->put( 87 | $this->parser->getBladeContent($file->getContents()), 88 | $file->getPathname(), 89 | '.blade.php', 90 | ); 91 | 92 | return $this->render($bladeFilePath, $pageData); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Handlers/CollectionItemHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | $this->handlers = collect($handlers); 20 | } 21 | 22 | public function shouldHandle($file) 23 | { 24 | return $this->isInCollectionDirectory($file) 25 | && ! Str::startsWith($file->getFilename(), ['.', '_']); 26 | } 27 | 28 | private function isInCollectionDirectory($file) 29 | { 30 | $base = $file->topLevelDirectory(); 31 | 32 | return Str::startsWith($base, '_') && $this->hasCollectionNamed($this->getCollectionName($file)); 33 | } 34 | 35 | private function hasCollectionNamed($candidate) 36 | { 37 | return Arr::get($this->config, 'collections.' . $candidate) !== null; 38 | } 39 | 40 | private function getCollectionName($file) 41 | { 42 | return substr($file->topLevelDirectory(), 1); 43 | } 44 | 45 | public function handle($file, $pageData) 46 | { 47 | $handler = $this->handlers->first(function ($handler) use ($file) { 48 | return $handler->shouldHandle($file); 49 | }); 50 | $pageData->setPageVariableToCollectionItem($this->getCollectionName($file), $file->getFilenameWithoutExtension()); 51 | 52 | if ($pageData->page === null) { 53 | return null; 54 | } 55 | 56 | return $handler->handleCollectionItem($file, $pageData) 57 | ->map(function ($outputFile, $templateToExtend) use ($file) { 58 | if ($templateToExtend) { 59 | $outputFile->data()->setExtending($templateToExtend); 60 | } 61 | 62 | $path = $outputFile->data()->page->getPath(); 63 | 64 | return $path ? new OutputFile( 65 | $file, 66 | dirname($path), 67 | basename($path, '.' . $outputFile->extension()), 68 | $outputFile->extension(), 69 | $outputFile->contents(), 70 | $outputFile->data(), 71 | ) : null; 72 | })->filter()->values(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Handlers/DefaultHandler.php: -------------------------------------------------------------------------------- 1 | files = $files; 15 | } 16 | 17 | public function shouldHandle($file) 18 | { 19 | return true; 20 | } 21 | 22 | public function handle($file, $pageData) 23 | { 24 | return collect([ 25 | new CopyFile( 26 | $file, 27 | $file->getPathName(), 28 | $file->getRelativePath(), 29 | $file->getBasename('.' . $file->getExtension()), 30 | $file->getExtension(), 31 | $pageData, 32 | ), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Handlers/IgnoredHandler.php: -------------------------------------------------------------------------------- 1 | getRelativePathname()) === 1; 10 | } 11 | 12 | public function handle($file, $data) 13 | { 14 | return collect([]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Handlers/MarkdownHandler.php: -------------------------------------------------------------------------------- 1 | temporaryFilesystem = $temporaryFilesystem; 22 | $this->parser = $parser; 23 | $this->view = $viewRenderer; 24 | } 25 | 26 | public function shouldHandle($file) 27 | { 28 | return in_array($file->getExtension(), ['markdown', 'md', 'mdown']); 29 | } 30 | 31 | public function handleCollectionItem($file, PageData $pageData) 32 | { 33 | return $this->buildOutput($file, $pageData); 34 | } 35 | 36 | public function handle($file, $pageData) 37 | { 38 | $pageData->page->addVariables($this->getPageVariables($file)); 39 | 40 | return $this->buildOutput($file, $pageData); 41 | } 42 | 43 | private function getPageVariables($file) 44 | { 45 | return array_merge(['section' => 'content'], $this->parseFrontMatter($file)); 46 | } 47 | 48 | private function buildOutput($file, PageData $pageData) 49 | { 50 | return collect($pageData->page->extends) 51 | ->map(function ($extends, $templateToExtend) use ($file, $pageData) { 52 | if ($templateToExtend) { 53 | $pageData->setExtending($templateToExtend); 54 | } 55 | 56 | $extension = $this->view->getExtension($extends); 57 | 58 | return new OutputFile( 59 | $file, 60 | $file->getRelativePath(), 61 | $file->getFileNameWithoutExtension(), 62 | $extension == 'php' ? 'html' : $extension, 63 | $this->render($file, $pageData, $extends), 64 | $pageData, 65 | ); 66 | }); 67 | } 68 | 69 | private function render($file, $pageData, $extends) 70 | { 71 | $uniqueFileName = $file->getPathname() . $extends; 72 | 73 | if ($cached = $this->getValidCachedFile($file, $uniqueFileName)) { 74 | return $this->view->render($cached->getPathname(), $pageData); 75 | } elseif ($file->isBladeFile()) { 76 | return $this->renderBladeMarkdownFile($file, $uniqueFileName, $pageData, $extends); 77 | } 78 | 79 | return $this->renderMarkdownFile($file, $uniqueFileName, $pageData, $extends); 80 | } 81 | 82 | private function renderMarkdownFile($file, $uniqueFileName, $pageData, $extends) 83 | { 84 | $html = $this->parser->parseMarkdownWithoutFrontMatter( 85 | $this->getEscapedMarkdownContent($file), 86 | ); 87 | $wrapper = $this->view->renderString( 88 | "@extends('{$extends}')\n" . 89 | "@section('{$pageData->page->section}'){$html}@endsection", 90 | ); 91 | 92 | return $this->view->render( 93 | $this->temporaryFilesystem->put($wrapper, $uniqueFileName, '.php'), 94 | $pageData, 95 | ); 96 | } 97 | 98 | private function renderBladeMarkdownFile($file, $uniqueFileName, $pageData, $extends) 99 | { 100 | $contentPath = $this->renderMarkdownContent($file); 101 | 102 | return $this->view->render( 103 | $this->renderBladeWrapper( 104 | $uniqueFileName, 105 | basename($contentPath, '.blade.md'), 106 | $pageData, 107 | $extends, 108 | ), 109 | $pageData, 110 | ); 111 | } 112 | 113 | private function renderMarkdownContent($file) 114 | { 115 | return $this->temporaryFilesystem->put( 116 | $this->getEscapedMarkdownContent($file), 117 | $file->getPathname(), 118 | '.blade.md', 119 | ); 120 | } 121 | 122 | private function renderBladeWrapper($sourceFileName, $contentFileName, $pageData, $extends) 123 | { 124 | return $this->temporaryFilesystem->put( 125 | $this->makeBladeWrapper($contentFileName, $pageData, $extends), 126 | $sourceFileName, 127 | '.blade.php', 128 | ); 129 | } 130 | 131 | private function makeBladeWrapper($path, $pageData, $extends) 132 | { 133 | return collect([ 134 | "@extends('{$extends}')", 135 | "@section('{$pageData->page->section}')", 136 | "@include('{$path}')", 137 | '@endsection', 138 | ])->implode("\n"); 139 | } 140 | 141 | private function getValidCachedFile($file, $uniqueFileName) 142 | { 143 | $extension = $file->isBladeFile() ? '.blade.md' : '.php'; 144 | $cached = $this->temporaryFilesystem->get($uniqueFileName, $extension); 145 | 146 | if ($cached && $cached->getLastModifiedTime() >= $file->getLastModifiedTime()) { 147 | return $cached; 148 | } 149 | } 150 | 151 | private function getEscapedMarkdownContent($file) 152 | { 153 | $replacements = [' "<{{'?php'}}"]; 154 | 155 | if (in_array($file->getFullExtension(), ['markdown', 'md', 'mdown'])) { 156 | $replacements = array_merge([ 157 | '@' => "{{'@'}}", 158 | '{{' => '@{{', 159 | '{!!' => '@{!!', 160 | ], $replacements); 161 | } 162 | 163 | return strtr($file->getContents(), $replacements); 164 | } 165 | 166 | private function parseFrontMatter($file) 167 | { 168 | return $this->parser->getFrontMatter($file->getContents()); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Handlers/PaginatedPageHandler.php: -------------------------------------------------------------------------------- 1 | paginator = $paginator; 30 | $this->parser = $parser; 31 | $this->temporaryFilesystem = $temporaryFilesystem; 32 | $this->view = $viewRenderer; 33 | } 34 | 35 | public function shouldHandle($file) 36 | { 37 | if (! Str::endsWith($file->getFilename(), ['.blade.md', '.blade.php'])) { 38 | return false; 39 | } 40 | $content = $this->parser->parse($file->getContents()); 41 | 42 | return isset($content->frontMatter['pagination']); 43 | } 44 | 45 | public function handle($file, PageData $pageData) 46 | { 47 | $page = $pageData->page; 48 | $page->addVariables($this->getPageVariables($file)); 49 | $collection = $page->pagination->collection; 50 | $prefix = $page->pagination->prefix 51 | ?: $page->collections->{$collection}->prefix 52 | ?: $page->prefix 53 | ?: ''; 54 | 55 | return $this->paginator->paginate( 56 | $file, 57 | $pageData->get($collection), 58 | $page->pagination->perPage 59 | ?: $page->collections->{$collection}->perPage 60 | ?: $page->perPage 61 | ?: 10, 62 | $prefix, 63 | )->map(function ($page) use ($file, $pageData, $prefix) { 64 | $pageData->setPagePath($page->current); 65 | $pageData->put('pagination', $page); 66 | $extension = strtolower($file->getExtension()); 67 | 68 | return new OutputFile( 69 | $file, 70 | $file->getRelativePath(), 71 | $file->getFilenameWithoutExtension(), 72 | ($extension == 'php' || $extension == 'md') ? 'html' : $extension, 73 | $this->render($file, $pageData), 74 | $pageData, 75 | $page->currentPage, 76 | $prefix, 77 | ); 78 | }); 79 | } 80 | 81 | private function getPageVariables($file) 82 | { 83 | return $this->parser->getFrontMatter($file->getContents()); 84 | } 85 | 86 | private function render($file, $pageData) 87 | { 88 | $bladeContent = $this->parser->getBladeContent($file->getContents()); 89 | $bladeFilePath = $this->temporaryFilesystem->put( 90 | $bladeContent, 91 | $file->getPathname(), 92 | '.blade.php', 93 | ); 94 | 95 | return $this->view->render($bladeFilePath, $pageData); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/IterableObject.php: -------------------------------------------------------------------------------- 1 | items) && in_array($key, static::$proxies)) { 18 | return new HigherOrderCollectionProxy($this, $key); 19 | } 20 | 21 | return $this->get($key); 22 | } 23 | 24 | public function except($keys) 25 | { 26 | return is_null($keys) ? $this : parent::except($keys); 27 | } 28 | 29 | public function get($key, $default = null) 30 | { 31 | if (array_key_exists($key, $this->items)) { 32 | return $this->getElement($key); 33 | } 34 | 35 | return value($default); 36 | } 37 | 38 | public function has($key) 39 | { 40 | $keys = is_array($key) ? $key : func_get_args(); 41 | 42 | foreach ($keys as $value) { 43 | if (! array_key_exists($value, $this->items)) { 44 | return false; 45 | } 46 | } 47 | 48 | return true; 49 | } 50 | 51 | public function set($key, $value) 52 | { 53 | data_set($this->items, $key, $this->isArrayable($value) ? $this->makeIterable($value) : $value); 54 | 55 | if ($first_key = Arr::get(explode('.', $key), 0)) { 56 | $this->putIterable($first_key, $this->get($first_key)); 57 | } 58 | } 59 | 60 | public function putIterable($key, $element) 61 | { 62 | $this->put($key, $this->isArrayable($element) ? $this->makeIterable($element) : $element); 63 | } 64 | 65 | public function offsetGet($key): mixed 66 | { 67 | if (! isset($this->items[$key])) { 68 | $prefix = $this->_source ? 'Error in ' . $this->_source . ': ' : 'Error: '; 69 | 70 | throw new Exception($prefix . "The key '$key' does not exist."); 71 | } 72 | 73 | return $this->getElement($key); 74 | } 75 | 76 | protected function getElement($key) 77 | { 78 | return $this->items[$key]; 79 | } 80 | 81 | protected function makeIterable($items) 82 | { 83 | if ($items instanceof IterableObject) { 84 | return $items; 85 | } 86 | 87 | return new IterableObject(collect($items)->map(function ($item) { 88 | return $this->isArrayable($item) ? $this->makeIterable($item) : $item; 89 | })); 90 | } 91 | 92 | protected function isArrayable($element) 93 | { 94 | return is_array($element) || $element instanceof BaseCollection; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/IterableObjectWithDefault.php: -------------------------------------------------------------------------------- 1 | first() ?: ''; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Jigsaw.php: -------------------------------------------------------------------------------- 1 | app = $app; 42 | $this->dataLoader = $dataLoader; 43 | $this->remoteItemLoader = $remoteItemLoader; 44 | $this->siteBuilder = $siteBuilder; 45 | } 46 | 47 | public function build($env, $useCache = false) 48 | { 49 | $this->env = $env; 50 | $this->siteData = $this->dataLoader->loadSiteData($this->app->config); 51 | 52 | return $this->fireEvent('beforeBuild') 53 | ->buildCollections() 54 | ->fireEvent('afterCollections') 55 | ->buildSite($useCache) 56 | ->fireEvent('afterBuild') 57 | ->cleanup(); 58 | } 59 | 60 | public static function registerCommand($command) 61 | { 62 | self::$commands[] = $command; 63 | } 64 | 65 | public static function addUserCommands($app, $container) 66 | { 67 | foreach (self::$commands as $command) { 68 | $app->add(new $command($container)); 69 | } 70 | } 71 | 72 | protected function buildCollections() 73 | { 74 | $this->remoteItemLoader->write($this->siteData->collections, $this->getSourcePath()); 75 | $collectionData = $this->dataLoader->loadCollectionData($this->siteData, $this->getSourcePath()); 76 | $this->siteData = $this->siteData->addCollectionData($collectionData); 77 | 78 | return $this; 79 | } 80 | 81 | protected function buildSite($useCache) 82 | { 83 | $this->pageInfo = $this->siteBuilder 84 | ->setUseCache($useCache) 85 | ->build( 86 | $this->getSourcePath(), 87 | $this->getDestinationPath(), 88 | $this->siteData, 89 | ); 90 | $this->outputPaths = $this->pageInfo->keys(); 91 | 92 | return $this; 93 | } 94 | 95 | protected function cleanup() 96 | { 97 | $this->remoteItemLoader->cleanup(); 98 | 99 | return $this; 100 | } 101 | 102 | protected function fireEvent($event) 103 | { 104 | $this->app->events->fire($event, $this); 105 | 106 | return $this; 107 | } 108 | 109 | public function getSiteData() 110 | { 111 | return $this->siteData; 112 | } 113 | 114 | public function getEnvironment() 115 | { 116 | return $this->env; 117 | } 118 | 119 | public function getCollection($collection) 120 | { 121 | return $this->siteData->get($collection); 122 | } 123 | 124 | public function getCollections() 125 | { 126 | return $this->siteData->get('collections') ? 127 | $this->siteData->get('collections')->keys() : 128 | $this->siteData->except('page'); 129 | } 130 | 131 | public function getConfig($key = null) 132 | { 133 | return $key ? data_get($this->siteData->page, $key) : $this->siteData->page; 134 | } 135 | 136 | public function setConfig($key, $value) 137 | { 138 | $this->siteData->set($key, $value); 139 | $this->siteData->page->set($key, $value); 140 | 141 | return $this; 142 | } 143 | 144 | public function getSourcePath() 145 | { 146 | return $this->app->buildPath['source']; 147 | } 148 | 149 | public function setSourcePath($path) 150 | { 151 | $this->app->buildPath = [ 152 | 'source' => $path, 153 | 'destination' => $this->app->buildPath['destination'], 154 | ]; 155 | 156 | return $this; 157 | } 158 | 159 | public function getDestinationPath() 160 | { 161 | return $this->app->buildPath['destination']; 162 | } 163 | 164 | public function setDestinationPath($path) 165 | { 166 | $this->app->buildPath = [ 167 | 'source' => $this->app->buildPath['source'], 168 | 'destination' => $path, 169 | ]; 170 | 171 | return $this; 172 | } 173 | 174 | public function getFilesystem() 175 | { 176 | return $this->app->make(Filesystem::class); 177 | } 178 | 179 | public function getOutputPaths() 180 | { 181 | return $this->outputPaths ?: collect(); 182 | } 183 | 184 | public function getPages() 185 | { 186 | return $this->pageInfo ?: collect(); 187 | } 188 | 189 | public function readSourceFile($fileName) 190 | { 191 | return $this->getFilesystem()->get($this->getSourcePath() . '/' . $fileName); 192 | } 193 | 194 | public function writeSourceFile($fileName, $contents) 195 | { 196 | return $this->getFilesystem()->putWithDirectories($this->getSourcePath() . '/' . $fileName, $contents); 197 | } 198 | 199 | public function readOutputFile($fileName) 200 | { 201 | return $this->getFilesystem()->get($this->getDestinationPath() . '/' . $fileName); 202 | } 203 | 204 | public function writeOutputFile($fileName, $contents) 205 | { 206 | return $this->getFilesystem()->putWithDirectories($this->getDestinationPath() . '/' . $fileName, $contents); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Loaders/CollectionDataLoader.php: -------------------------------------------------------------------------------- 1 | filesystem = $filesystem; 35 | $this->pathResolver = $pathResolver; 36 | $this->handlers = collect($handlers); 37 | $this->consoleOutput = $consoleOutput; 38 | } 39 | 40 | public function load($siteData, $source) 41 | { 42 | $this->source = $source; 43 | $this->pageSettings = $siteData->page; 44 | $this->collectionSettings = collect($siteData->collections); 45 | $this->consoleOutput->startProgressBar('collections'); 46 | 47 | $collections = $this->collectionSettings->map(function ($collectionSettings, $collectionName) { 48 | $collection = Collection::withSettings($collectionSettings, $collectionName); 49 | $collection->loadItems($this->buildCollection($collection)); 50 | 51 | return $collection->updateItems($collection->map(function ($item) { 52 | return $this->addCollectionItemContent($item); 53 | })); 54 | }); 55 | 56 | return $collections->all(); 57 | } 58 | 59 | private function buildCollection($collection) 60 | { 61 | $path = "{$this->source}/_{$collection->name}"; 62 | 63 | if (! $this->filesystem->exists($path)) { 64 | return collect(); 65 | } 66 | 67 | return collect($this->filesystem->files($path)) 68 | ->reject(function ($file) { 69 | return Str::startsWith($file->getFilename(), '_'); 70 | })->filter(function ($file) { 71 | return $this->hasHandler($file); 72 | })->tap(function ($files) { 73 | $this->consoleOutput->progressBar('collections')->addSteps($files->count()); 74 | })->map(function ($file) { 75 | return new InputFile($file); 76 | })->map(function ($inputFile) use ($collection) { 77 | $this->consoleOutput->progressBar('collections')->advance(); 78 | 79 | return $this->buildCollectionItem($inputFile, $collection); 80 | }); 81 | } 82 | 83 | private function buildCollectionItem($file, $collection) 84 | { 85 | $data = $this->pageSettings 86 | ->merge(['section' => 'content']) 87 | ->merge($collection->settings) 88 | ->merge($this->getHandler($file)->getItemVariables($file)); 89 | $data->put('_meta', new IterableObject($this->getMetaData($file, $collection, $data))); 90 | $path = $this->getPath($data, $collection); 91 | $data->_meta->put('path', $path)->put('url', $this->buildUrls($path)); 92 | 93 | return CollectionItem::build($collection, $data); 94 | } 95 | 96 | private function addCollectionItemContent($item) 97 | { 98 | $file = $this->filesystem->getFile($item->getSource(), $item->getFilename() . '.' . $item->getExtension()); 99 | 100 | if ($file) { 101 | $item->setContent($this->getHandler($file)->getItemContent($file)); 102 | } 103 | 104 | return $item; 105 | } 106 | 107 | private function hasHandler($file): bool 108 | { 109 | return $this->handlers->contains(function ($handler) use ($file) { 110 | return $handler->shouldHandle($file); 111 | }); 112 | } 113 | 114 | private function getHandler($file) 115 | { 116 | return $this->handlers->first(function ($handler) use ($file) { 117 | return $handler->shouldHandle($file); 118 | }); 119 | } 120 | 121 | private function getMetaData($file, $collection, $data) 122 | { 123 | $filename = $file->getFilenameWithoutExtension(); 124 | $baseUrl = $data->baseUrl; 125 | $relativePath = $file->getRelativePath(); 126 | $extension = $file->getFullExtension(); 127 | $collectionName = $collection->name; 128 | $collection = $collectionName; 129 | $source = $file->getPath(); 130 | $modifiedTime = $file->getLastModifiedTime(); 131 | 132 | return compact('filename', 'baseUrl', 'relativePath', 'extension', 'collection', 'collectionName', 'source', 'modifiedTime'); 133 | } 134 | 135 | private function buildUrls($paths) 136 | { 137 | $urls = collect($paths)->map(function ($path) { 138 | return rightTrimPath($this->pageSettings->get('baseUrl')) . '/' . trimPath($path); 139 | }); 140 | 141 | return $urls->count() ? new IterableObjectWithDefault($urls) : null; 142 | } 143 | 144 | private function getPath($data, $collection) 145 | { 146 | $links = $this->pathResolver->link( 147 | $data->path, 148 | new PageVariable($data), 149 | Arr::get($collection->settings, 'transliterate', true), 150 | ); 151 | 152 | return $links->count() ? new IterableObjectWithDefault($links) : null; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Loaders/CollectionRemoteItemLoader.php: -------------------------------------------------------------------------------- 1 | config = $config; 20 | $this->files = $files; 21 | } 22 | 23 | public function write($collections, $source) 24 | { 25 | collect($collections)->each(function ($collection, $collectionName) use ($source) { 26 | $items = $this->getItems($collection); 27 | 28 | if (collect($items)->count()) { 29 | $this->writeTempFiles($items, $this->createTempDirectory($source, $collectionName), $collectionName); 30 | } 31 | }); 32 | } 33 | 34 | private function createTempDirectory($source, $collectionName) 35 | { 36 | $tempDirectory = $source . '/_' . $collectionName . '/_tmp'; 37 | $this->prepareDirectory($tempDirectory, true); 38 | $this->tempDirectories[] = $tempDirectory; 39 | 40 | return $tempDirectory; 41 | } 42 | 43 | public function cleanup() 44 | { 45 | collect($this->tempDirectories)->each(function ($path) { 46 | $this->files->deleteDirectory($path); 47 | 48 | if ($this->files->isEmptyDirectory($parent = $this->files->dirname($path))) { 49 | $this->files->deleteDirectory($parent); 50 | } 51 | }); 52 | } 53 | 54 | private function getItems($collection) 55 | { 56 | if (! $collection->items) { 57 | return; 58 | } 59 | 60 | return is_callable($collection->items) ? 61 | $collection->items->__invoke($this->config) : 62 | $collection->items->toArray(); 63 | } 64 | 65 | private function prepareDirectory($directory, $clean = false) 66 | { 67 | if (! $this->files->isDirectory($directory)) { 68 | $this->files->makeDirectory($directory, 0755, true); 69 | } 70 | 71 | if ($clean) { 72 | $this->files->cleanDirectory($directory); 73 | } 74 | } 75 | 76 | private function writeTempFiles($items, $directory, $collectionName) 77 | { 78 | collect($items)->each(function ($item, $index) use ($directory, $collectionName) { 79 | $this->writeFile(new CollectionRemoteItem($item, $index, $collectionName), $directory); 80 | }); 81 | } 82 | 83 | private function writeFile($remoteFile, $directory) 84 | { 85 | $this->files->put($directory . '/' . $remoteFile->getFilename(), $remoteFile->getContent()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Loaders/DataLoader.php: -------------------------------------------------------------------------------- 1 | collectionDataLoader = $collectionDataLoader; 15 | } 16 | 17 | public function loadSiteData(Collection $config) 18 | { 19 | return SiteData::build($config); 20 | } 21 | 22 | public function loadCollectionData($siteData, $source) 23 | { 24 | return $this->collectionDataLoader->load($siteData, $source); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/PageData.php: -------------------------------------------------------------------------------- 1 | except('page')); 10 | $page_data->put('page', (new PageVariable($siteData->page))->put('_meta', new IterableObject($meta))); 11 | 12 | return $page_data; 13 | } 14 | 15 | public function setPageVariableToCollectionItem($collectionName, $itemName) 16 | { 17 | $this->put('page', $this->get($collectionName)->get($itemName)); 18 | } 19 | 20 | public function setExtending($templateToExtend) 21 | { 22 | $this->page->_meta->put('extending', $templateToExtend); 23 | } 24 | 25 | public function setPagePath($path) 26 | { 27 | $this->page->_meta->put('path', $path); 28 | $this->updatePageUrl(); 29 | } 30 | 31 | public function updatePageUrl() 32 | { 33 | $this->page->_meta->put('url', rightTrimPath($this->page->getBaseUrl()) . '/' . trimPath($this->page->getPath())); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/PageVariable.php: -------------------------------------------------------------------------------- 1 | items = collect($this->items)->merge($this->makeIterable($variables))->all(); 13 | } 14 | 15 | public function __call($method, $args) 16 | { 17 | $helper = $this->get($method); 18 | 19 | if (! $helper && Str::startsWith($method, 'get')) { 20 | return $this->_meta->get(Str::camel(substr($method, 3)), function () use ($method) { 21 | throw new Exception($this->missingHelperError($method)); 22 | }); 23 | } 24 | 25 | if (is_callable($helper)) { 26 | return $helper->__invoke($this, ...$args); 27 | } 28 | 29 | throw new Exception($this->missingHelperError($method)); 30 | } 31 | 32 | public function getPath($key = null) 33 | { 34 | if (($key || $this->_meta->extending) && $this->_meta->path instanceof IterableObject) { 35 | return $this->enforceTrailingSlash($this->_meta->path->get($key ?: $this->getExtending())); 36 | } 37 | 38 | return $this->enforceTrailingSlash((string) $this->_meta->path); 39 | } 40 | 41 | public function getPaths() 42 | { 43 | return $this->_meta->path; 44 | } 45 | 46 | public function getUrl($key = null) 47 | { 48 | if (($key || $this->_meta->extending) && $this->_meta->path instanceof IterableObject) { 49 | return $this->enforceTrailingSlash($this->_meta->url->get($key ?: $this->getExtending())); 50 | } 51 | 52 | return $this->enforceTrailingSlash((string) $this->_meta->url); 53 | } 54 | 55 | public function getUrls() 56 | { 57 | return $this->_meta->url; 58 | } 59 | 60 | protected function missingHelperError($functionName) 61 | { 62 | return 'No function named "' . $functionName . '" was found in the file "config.php".'; 63 | } 64 | 65 | protected function enforceTrailingSlash($path) 66 | { 67 | return $path && app()->config->get('trailing_slash') && ! $this->pathIsFile($path) 68 | ? Str::finish($path, '/') 69 | : $path; 70 | } 71 | 72 | protected function pathIsFile($path) 73 | { 74 | $final_extension = $this->_meta->extending 75 | ? (Str::contains(Str::afterLast($path, '/'), '.') ? Str::afterLast($path, '.') : null) 76 | : Str::afterLast($this->_meta->extension, '.'); 77 | 78 | return $final_extension && $final_extension !== 'md' && $final_extension !== 'php'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Parsers/CommonMarkParser.php: -------------------------------------------------------------------------------- 1 | addExtension(new CommonMarkCoreExtension); 23 | 24 | collect(Arr::get(app('config'), 'commonmark.extensions', [ 25 | new AttributesExtension, 26 | new SmartPunctExtension, 27 | new StrikethroughExtension, 28 | new TableExtension, 29 | ]))->map(fn ($extension) => $environment->addExtension($extension)); 30 | 31 | collect( 32 | Arr::get(app('config'), 'commonmark.renderers') 33 | )->map(fn ($renderer, $nodeClass) => $environment->addRenderer($nodeClass, $renderer)); 34 | 35 | $this->converter = new MarkdownConverter($environment); 36 | } 37 | 38 | public function parse(string $text) 39 | { 40 | return $this->converter->convert($text); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Parsers/FrontMatterParser.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 19 | } 20 | 21 | public function parseMarkdown($content) 22 | { 23 | return $this->parse($content, true)->content; 24 | } 25 | 26 | public function parseMarkdownWithoutFrontMatter($content) 27 | { 28 | return $this->parser->parse($this->extractContent($content))->getContent(); 29 | } 30 | 31 | public function parse($content, $parseMarkdown = false) 32 | { 33 | $document = $this->parser->parse($content, $parseMarkdown); 34 | $this->frontMatter = $document->getYAML() !== null ? $document->getYAML() : []; 35 | $this->content = $document->getContent(); 36 | 37 | return $this; 38 | } 39 | 40 | public function getFrontMatter($content) 41 | { 42 | return $this->parse($content)->frontMatter; 43 | } 44 | 45 | public function getContent($content) 46 | { 47 | return $this->parse($content, false)->content; 48 | } 49 | 50 | public function getBladeContent($content) 51 | { 52 | $parsed = $this->parse($content); 53 | $extendsFromFrontMatter = Arr::get($parsed->frontMatter, 'extends'); 54 | 55 | return (! $this->getExtendsFromBladeContent($parsed->content) && $extendsFromFrontMatter) ? 56 | $this->addExtendsToBladeContent($extendsFromFrontMatter, $parsed->content) : 57 | $parsed->content; 58 | } 59 | 60 | public function getExtendsFromBladeContent($content) 61 | { 62 | preg_match('/@extends\s*\(\s*[\"|\']\s*(.+?)\s*[\"|\']\s*\)/', $content, $matches); 63 | 64 | return isset($matches[1]) ? $matches[1] : null; 65 | } 66 | 67 | /** 68 | * Adapted from Mni\FrontYAML. 69 | */ 70 | public function extractContent($content) 71 | { 72 | $regex = '~^(' 73 | . '---' // $matches[1] start separator 74 | . "){1}[\r\n|\n]*(.*?)[\r\n|\n]+(" // $matches[2] front matter 75 | . '---' // $matches[3] end separator 76 | . "){1}[\r\n|\n]*(.*)$~s"; // $matches[4] document content 77 | 78 | return preg_match($regex, $content, $matches) === 1 ? ltrim($matches[4]) : $content; 79 | } 80 | 81 | private function addExtendsToBladeContent($extends, $bladeContent) 82 | { 83 | return "@extends('$extends')\n" . $bladeContent; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Parsers/JigsawMarkdownParser.php: -------------------------------------------------------------------------------- 1 | code_class_prefix = 'language-'; 13 | $this->url_filter_func = function ($url) { 14 | return str_replace("{{'@'}}", '@', $url); 15 | }; 16 | } 17 | 18 | public function text($text) 19 | { 20 | return $this->transform($text); 21 | } 22 | 23 | public function parse($text) 24 | { 25 | return $this->text($text); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Parsers/MarkdownParser.php: -------------------------------------------------------------------------------- 1 | parser = $parser ?? new JigsawMarkdownParser; 14 | } 15 | 16 | public function __get($property) 17 | { 18 | return $this->parser->$property; 19 | } 20 | 21 | public function __set($property, $value) 22 | { 23 | $this->parser->$property = $value; 24 | } 25 | 26 | public function parse($markdown): string 27 | { 28 | return $this->parser->parse($markdown); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Parsers/MarkdownParserContract.php: -------------------------------------------------------------------------------- 1 | 1 ? 13 | $this->clean('/' . $path . '/' . $page . '/' . $name . $extension) : 14 | $this->clean('/' . $path . '/' . $name . $extension); 15 | } 16 | 17 | public function path($path, $name, $type, $page = 1) 18 | { 19 | return $this->link($path, $name, $type, $page); 20 | } 21 | 22 | public function directory($path, $name, $type, $page = 1) 23 | { 24 | return $page > 1 ? 25 | $this->clean($path . '/' . $page) : 26 | $this->clean($path); 27 | } 28 | 29 | private function clean($path) 30 | { 31 | return str_replace('//', '/', $path); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PathResolvers/CollectionPathResolver.php: -------------------------------------------------------------------------------- 1 | outputPathResolver = $outputPathResolver; 17 | $this->view = $viewRenderer; 18 | } 19 | 20 | public function link($path, $data, bool $transliterate = true) 21 | { 22 | return collect($data->extends)->map(function ($bladeViewPath, $templateKey) use ($path, $data, $transliterate) { 23 | return $this->cleanOutputPath( 24 | $this->getPath($path, $data, $this->getExtension($bladeViewPath), $templateKey), 25 | $transliterate, 26 | ); 27 | }); 28 | } 29 | 30 | public function getExtension($bladeViewPath) 31 | { 32 | $extension = $this->view->getExtension($bladeViewPath); 33 | 34 | return collect(['php', 'html'])->contains($extension) ? '' : '.' . $extension; 35 | } 36 | 37 | private function getPath($path, $data, $extension, $templateKey = null) 38 | { 39 | $templateKeySuffix = $templateKey ? '/' . $templateKey : ''; 40 | 41 | if ($templateKey && $path instanceof IterableObject) { 42 | $path = $path->get($templateKey); 43 | $templateKeySuffix = ''; 44 | 45 | if (! $path) { 46 | return; 47 | } 48 | } 49 | 50 | if (is_callable($path)) { 51 | $link = $this->cleanInputPath($path->__invoke($data)); 52 | 53 | return $link ? $this->resolve($link . $templateKeySuffix . $extension) : ''; 54 | } 55 | 56 | if (is_string($path) && $path) { 57 | $link = $this->parseShorthand($this->cleanInputPath($path), $data); 58 | 59 | return $link ? $this->resolve($link . $templateKeySuffix . $extension) : ''; 60 | } 61 | 62 | return $this->getDefaultPath($data, $templateKey) . $templateKeySuffix . $extension; 63 | } 64 | 65 | private function getDefaultPath($data) 66 | { 67 | return $this->slug($data->getCollectionName()) . '/' . $this->slug($data->getFilename()); 68 | } 69 | 70 | private function parseShorthand($path, $data) 71 | { 72 | preg_match_all('/\{(.*?)\}/', $path, $bracketedParameters); 73 | 74 | if (count($bracketedParameters[0]) == 0) { 75 | return $path . '/' . $this->slug($data->getFilename()); 76 | } 77 | 78 | $bracketedParametersReplaced = 79 | collect($bracketedParameters[0])->map(function ($param) use ($data) { 80 | return ['token' => $param, 'value' => $this->getParameterValue($param, $data)]; 81 | })->reduce(function ($carry, $param) { 82 | return str_replace($param['token'], $param['value'], $carry); 83 | }, $path); 84 | 85 | return $bracketedParametersReplaced; 86 | } 87 | 88 | private function getParameterValue($param, $data) 89 | { 90 | [$param, $dateFormat] = explode('|', trim($param, '{}') . '|'); 91 | $slugSeparator = ctype_alpha($param[0]) ? null : $param[0]; 92 | 93 | if ($slugSeparator) { 94 | $param = ltrim($param, $param[0]); 95 | } 96 | 97 | $value = Arr::get($data, $param, $data->_meta->get($param)); 98 | 99 | if (! $value) { 100 | return ''; 101 | } 102 | 103 | $value = $dateFormat ? $this->formatDate($value, $dateFormat) : $value; 104 | 105 | return $slugSeparator ? $this->slug($value, $slugSeparator) : $value; 106 | } 107 | 108 | private function formatDate($date, $format) 109 | { 110 | if (is_string($date)) { 111 | return strtotime($date) ? date($format, strtotime($date)) : ''; 112 | } 113 | 114 | return date($format, $date); 115 | } 116 | 117 | private function cleanInputPath($path) 118 | { 119 | return $this->ensureSlashAtBeginningOnly($path); 120 | } 121 | 122 | private function cleanOutputPath($path, bool $transliterate) 123 | { 124 | // Remove double slashes 125 | $path = preg_replace('/\/\/+/', '/', $path); 126 | 127 | if ($transliterate) { 128 | $path = $this->ascii($path); 129 | } 130 | 131 | return $this->ensureSlashAtBeginningOnly($path); 132 | } 133 | 134 | private function ensureSlashAtBeginningOnly($path) 135 | { 136 | return '/' . trimPath($path); 137 | } 138 | 139 | private function resolve($path) 140 | { 141 | return $this->outputPathResolver->link(dirname($path), basename($path), 'html'); 142 | } 143 | 144 | /** 145 | * This is identical to Laravel's built-in `str_slug()` helper, 146 | * except it preserves `.` characters. 147 | */ 148 | private function slug($string, $separator = '-') 149 | { 150 | // Convert all dashes/underscores into separator 151 | $flip = $separator == '-' ? '_' : '-'; 152 | $string = preg_replace('![' . preg_quote($flip) . ']+!u', $separator, $string); 153 | 154 | // Remove all characters that are not the separator, letters, numbers, whitespace, or dot 155 | $string = preg_replace('![^' . preg_quote($separator) . '\pL\pN\s\.]+!u', '', mb_strtolower($string)); 156 | 157 | // Replace all separator characters and whitespace by a single separator 158 | $string = preg_replace('![' . preg_quote($separator) . '\s]+!u', $separator, $string); 159 | 160 | return trim($string, $separator); 161 | } 162 | 163 | /** 164 | * Transliterate a UTF-8 value to ASCII. 165 | * 166 | * @param string $value 167 | * @param string $language 168 | * @return string 169 | */ 170 | private static function ascii($value, $language = 'en') 171 | { 172 | $languageSpecific = static::languageSpecificCharsArray($language); 173 | 174 | if (! is_null($languageSpecific)) { 175 | $value = str_replace($languageSpecific[0], $languageSpecific[1], $value); 176 | } 177 | 178 | foreach (static::charsArray() as $key => $val) { 179 | $value = str_replace($val, $key, $value); 180 | } 181 | 182 | return preg_replace('/[\x{00a1}-\x{00bf}|\x{2120}\x{2122}]/u', '', $value); 183 | } 184 | 185 | /** 186 | * Returns the replacements for the ascii method. 187 | * 188 | * Note: Adapted from Stringy\Stringy. 189 | * 190 | * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt 191 | * 192 | * @return array 193 | */ 194 | private static function charsArray() 195 | { 196 | static $charsArray; 197 | 198 | if (isset($charsArray)) { 199 | return $charsArray; 200 | } 201 | 202 | return $charsArray = [ 203 | '0' => ['°', '₀', '۰', '0'], 204 | '1' => ['¹', '₁', '۱', '1'], 205 | '2' => ['²', '₂', '۲', '2'], 206 | '3' => ['³', '₃', '۳', '3'], 207 | '4' => ['⁴', '₄', '۴', '٤', '4'], 208 | '5' => ['⁵', '₅', '۵', '٥', '5'], 209 | '6' => ['⁶', '₆', '۶', '٦', '6'], 210 | '7' => ['⁷', '₇', '۷', '7'], 211 | '8' => ['⁸', '₈', '۸', '8'], 212 | '9' => ['⁹', '₉', '۹', '9'], 213 | 'a' => ['à', 'á', 'ả', 'ã', 'ạ', 'ă', 'ắ', 'ằ', 'ẳ', 'ẵ', 'ặ', 'â', 'ấ', 'ầ', 'ẩ', 'ẫ', 'ậ', 'ā', 'ą', 'å', 'α', 'ά', 'ἀ', 'ἁ', 'ἂ', 'ἃ', 'ἄ', 'ἅ', 'ἆ', 'ἇ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ὰ', 'ά', 'ᾰ', 'ᾱ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'а', 'أ', 'အ', 'ာ', 'ါ', 'ǻ', 'ǎ', 'ª', 'ა', 'अ', 'ا', 'a', 'ä'], 214 | 'b' => ['б', 'β', 'ب', 'ဗ', 'ბ', 'b'], 215 | 'c' => ['ç', 'ć', 'č', 'ĉ', 'ċ', 'c'], 216 | 'd' => ['ď', 'ð', 'đ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ᵭ', 'ᶁ', 'ᶑ', 'д', 'δ', 'د', 'ض', 'ဍ', 'ဒ', 'დ', 'd'], 217 | 'e' => ['é', 'è', 'ẻ', 'ẽ', 'ẹ', 'ê', 'ế', 'ề', 'ể', 'ễ', 'ệ', 'ë', 'ē', 'ę', 'ě', 'ĕ', 'ė', 'ε', 'έ', 'ἐ', 'ἑ', 'ἒ', 'ἓ', 'ἔ', 'ἕ', 'ὲ', 'έ', 'е', 'ё', 'э', 'є', 'ə', 'ဧ', 'ေ', 'ဲ', 'ე', 'ए', 'إ', 'ئ', 'e'], 218 | 'f' => ['ф', 'φ', 'ف', 'ƒ', 'ფ', 'f'], 219 | 'g' => ['ĝ', 'ğ', 'ġ', 'ģ', 'г', 'ґ', 'γ', 'ဂ', 'გ', 'گ', 'g'], 220 | 'h' => ['ĥ', 'ħ', 'η', 'ή', 'ح', 'ه', 'ဟ', 'ှ', 'ჰ', 'h'], 221 | 'i' => ['í', 'ì', 'ỉ', 'ĩ', 'ị', 'î', 'ï', 'ī', 'ĭ', 'į', 'ı', 'ι', 'ί', 'ϊ', 'ΐ', 'ἰ', 'ἱ', 'ἲ', 'ἳ', 'ἴ', 'ἵ', 'ἶ', 'ἷ', 'ὶ', 'ί', 'ῐ', 'ῑ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'і', 'ї', 'и', 'ဣ', 'ိ', 'ီ', 'ည်', 'ǐ', 'ი', 'इ', 'ی', 'i'], 222 | 'j' => ['ĵ', 'ј', 'Ј', 'ჯ', 'ج', 'j'], 223 | 'k' => ['ķ', 'ĸ', 'к', 'κ', 'Ķ', 'ق', 'ك', 'က', 'კ', 'ქ', 'ک', 'k'], 224 | 'l' => ['ł', 'ľ', 'ĺ', 'ļ', 'ŀ', 'л', 'λ', 'ل', 'လ', 'ლ', 'l'], 225 | 'm' => ['м', 'μ', 'م', 'မ', 'მ', 'm'], 226 | 'n' => ['ñ', 'ń', 'ň', 'ņ', 'ʼn', 'ŋ', 'ν', 'н', 'ن', 'န', 'ნ', 'n'], 227 | 'o' => ['ó', 'ò', 'ỏ', 'õ', 'ọ', 'ô', 'ố', 'ồ', 'ổ', 'ỗ', 'ộ', 'ơ', 'ớ', 'ờ', 'ở', 'ỡ', 'ợ', 'ø', 'ō', 'ő', 'ŏ', 'ο', 'ὀ', 'ὁ', 'ὂ', 'ὃ', 'ὄ', 'ὅ', 'ὸ', 'ό', 'о', 'و', 'θ', 'ို', 'ǒ', 'ǿ', 'º', 'ო', 'ओ', 'o', 'ö'], 228 | 'p' => ['п', 'π', 'ပ', 'პ', 'پ', 'p'], 229 | 'q' => ['ყ', 'q'], 230 | 'r' => ['ŕ', 'ř', 'ŗ', 'р', 'ρ', 'ر', 'რ', 'r'], 231 | 's' => ['ś', 'š', 'ş', 'с', 'σ', 'ș', 'ς', 'س', 'ص', 'စ', 'ſ', 'ს', 's'], 232 | 't' => ['ť', 'ţ', 'т', 'τ', 'ț', 'ت', 'ط', 'ဋ', 'တ', 'ŧ', 'თ', 'ტ', 't'], 233 | 'u' => ['ú', 'ù', 'ủ', 'ũ', 'ụ', 'ư', 'ứ', 'ừ', 'ử', 'ữ', 'ự', 'û', 'ū', 'ů', 'ű', 'ŭ', 'ų', 'µ', 'у', 'ဉ', 'ု', 'ူ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'უ', 'उ', 'u', 'ў', 'ü'], 234 | 'v' => ['в', 'ვ', 'ϐ', 'v'], 235 | 'w' => ['ŵ', 'ω', 'ώ', 'ဝ', 'ွ', 'w'], 236 | 'x' => ['χ', 'ξ', 'x'], 237 | 'y' => ['ý', 'ỳ', 'ỷ', 'ỹ', 'ỵ', 'ÿ', 'ŷ', 'й', 'ы', 'υ', 'ϋ', 'ύ', 'ΰ', 'ي', 'ယ', 'y'], 238 | 'z' => ['ź', 'ž', 'ż', 'з', 'ζ', 'ز', 'ဇ', 'ზ', 'z'], 239 | 'aa' => ['ع', 'आ', 'آ'], 240 | 'ae' => ['æ', 'ǽ'], 241 | 'ai' => ['ऐ'], 242 | 'ch' => ['ч', 'ჩ', 'ჭ', 'چ'], 243 | 'dj' => ['ђ', 'đ'], 244 | 'dz' => ['џ', 'ძ'], 245 | 'ei' => ['ऍ'], 246 | 'gh' => ['غ', 'ღ'], 247 | 'ii' => ['ई'], 248 | 'ij' => ['ij'], 249 | 'kh' => ['х', 'خ', 'ხ'], 250 | 'lj' => ['љ'], 251 | 'nj' => ['њ'], 252 | 'oe' => ['ö', 'œ', 'ؤ'], 253 | 'oi' => ['ऑ'], 254 | 'oii' => ['ऒ'], 255 | 'ps' => ['ψ'], 256 | 'sh' => ['ш', 'შ', 'ش'], 257 | 'shch' => ['щ'], 258 | 'ss' => ['ß'], 259 | 'sx' => ['ŝ'], 260 | 'th' => ['þ', 'ϑ', 'ث', 'ذ', 'ظ'], 261 | 'ts' => ['ц', 'ც', 'წ'], 262 | 'ue' => ['ü'], 263 | 'uu' => ['ऊ'], 264 | 'ya' => ['я'], 265 | 'yu' => ['ю'], 266 | 'zh' => ['ж', 'ჟ', 'ژ'], 267 | '(c)' => ['©'], 268 | 'A' => ['Á', 'À', 'Ả', 'Ã', 'Ạ', 'Ă', 'Ắ', 'Ằ', 'Ẳ', 'Ẵ', 'Ặ', 'Â', 'Ấ', 'Ầ', 'Ẩ', 'Ẫ', 'Ậ', 'Å', 'Ā', 'Ą', 'Α', 'Ά', 'Ἀ', 'Ἁ', 'Ἂ', 'Ἃ', 'Ἄ', 'Ἅ', 'Ἆ', 'Ἇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'Ᾰ', 'Ᾱ', 'Ὰ', 'Ά', 'ᾼ', 'А', 'Ǻ', 'Ǎ', 'A', 'Ä'], 269 | 'B' => ['Б', 'Β', 'ब', 'B'], 270 | 'C' => ['Ç', 'Ć', 'Č', 'Ĉ', 'Ċ', 'C'], 271 | 'D' => ['Ď', 'Ð', 'Đ', 'Ɖ', 'Ɗ', 'Ƌ', 'ᴅ', 'ᴆ', 'Д', 'Δ', 'D'], 272 | 'E' => ['É', 'È', 'Ẻ', 'Ẽ', 'Ẹ', 'Ê', 'Ế', 'Ề', 'Ể', 'Ễ', 'Ệ', 'Ë', 'Ē', 'Ę', 'Ě', 'Ĕ', 'Ė', 'Ε', 'Έ', 'Ἐ', 'Ἑ', 'Ἒ', 'Ἓ', 'Ἔ', 'Ἕ', 'Έ', 'Ὲ', 'Е', 'Ё', 'Э', 'Є', 'Ə', 'E'], 273 | 'F' => ['Ф', 'Φ', 'F'], 274 | 'G' => ['Ğ', 'Ġ', 'Ģ', 'Г', 'Ґ', 'Γ', 'G'], 275 | 'H' => ['Η', 'Ή', 'Ħ', 'H'], 276 | 'I' => ['Í', 'Ì', 'Ỉ', 'Ĩ', 'Ị', 'Î', 'Ï', 'Ī', 'Ĭ', 'Į', 'İ', 'Ι', 'Ί', 'Ϊ', 'Ἰ', 'Ἱ', 'Ἳ', 'Ἴ', 'Ἵ', 'Ἶ', 'Ἷ', 'Ῐ', 'Ῑ', 'Ὶ', 'Ί', 'И', 'І', 'Ї', 'Ǐ', 'ϒ', 'I'], 277 | 'J' => ['J'], 278 | 'K' => ['К', 'Κ', 'K'], 279 | 'L' => ['Ĺ', 'Ł', 'Л', 'Λ', 'Ļ', 'Ľ', 'Ŀ', 'ल', 'L'], 280 | 'M' => ['М', 'Μ', 'M'], 281 | 'N' => ['Ń', 'Ñ', 'Ň', 'Ņ', 'Ŋ', 'Н', 'Ν', 'N'], 282 | 'O' => ['Ó', 'Ò', 'Ỏ', 'Õ', 'Ọ', 'Ô', 'Ố', 'Ồ', 'Ổ', 'Ỗ', 'Ộ', 'Ơ', 'Ớ', 'Ờ', 'Ở', 'Ỡ', 'Ợ', 'Ø', 'Ō', 'Ő', 'Ŏ', 'Ο', 'Ό', 'Ὀ', 'Ὁ', 'Ὂ', 'Ὃ', 'Ὄ', 'Ὅ', 'Ὸ', 'Ό', 'О', 'Θ', 'Ө', 'Ǒ', 'Ǿ', 'O', 'Ö'], 283 | 'P' => ['П', 'Π', 'P'], 284 | 'Q' => ['Q'], 285 | 'R' => ['Ř', 'Ŕ', 'Р', 'Ρ', 'Ŗ', 'R'], 286 | 'S' => ['Ş', 'Ŝ', 'Ș', 'Š', 'Ś', 'С', 'Σ', 'S'], 287 | 'T' => ['Ť', 'Ţ', 'Ŧ', 'Ț', 'Т', 'Τ', 'T'], 288 | 'U' => ['Ú', 'Ù', 'Ủ', 'Ũ', 'Ụ', 'Ư', 'Ứ', 'Ừ', 'Ử', 'Ữ', 'Ự', 'Û', 'Ū', 'Ů', 'Ű', 'Ŭ', 'Ų', 'У', 'Ǔ', 'Ǖ', 'Ǘ', 'Ǚ', 'Ǜ', 'U', 'Ў', 'Ü'], 289 | 'V' => ['В', 'V'], 290 | 'W' => ['Ω', 'Ώ', 'Ŵ', 'W'], 291 | 'X' => ['Χ', 'Ξ', 'X'], 292 | 'Y' => ['Ý', 'Ỳ', 'Ỷ', 'Ỹ', 'Ỵ', 'Ÿ', 'Ῠ', 'Ῡ', 'Ὺ', 'Ύ', 'Ы', 'Й', 'Υ', 'Ϋ', 'Ŷ', 'Y'], 293 | 'Z' => ['Ź', 'Ž', 'Ż', 'З', 'Ζ', 'Z'], 294 | 'AE' => ['Æ', 'Ǽ'], 295 | 'Ch' => ['Ч'], 296 | 'Dj' => ['Ђ'], 297 | 'Dz' => ['Џ'], 298 | 'Gx' => ['Ĝ'], 299 | 'Hx' => ['Ĥ'], 300 | 'Ij' => ['IJ'], 301 | 'Jx' => ['Ĵ'], 302 | 'Kh' => ['Х'], 303 | 'Lj' => ['Љ'], 304 | 'Nj' => ['Њ'], 305 | 'Oe' => ['Œ'], 306 | 'Ps' => ['Ψ'], 307 | 'Sh' => ['Ш'], 308 | 'Shch' => ['Щ'], 309 | 'Ss' => ['ẞ'], 310 | 'Th' => ['Þ'], 311 | 'Ts' => ['Ц'], 312 | 'Ya' => ['Я'], 313 | 'Yu' => ['Ю'], 314 | 'Zh' => ['Ж'], 315 | ' ' => ["\xC2\xA0", "\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", "\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", "\xE2\x80\x88", "\xE2\x80\x89", "\xE2\x80\x8A", "\xE2\x80\xAF", "\xE2\x81\x9F", "\xE3\x80\x80", "\xEF\xBE\xA0"], 316 | ]; 317 | } 318 | 319 | /** 320 | * Returns the language specific replacements for the ascii method. 321 | * 322 | * Note: Adapted from Stringy\Stringy. 323 | * 324 | * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt 325 | * 326 | * @param string $language 327 | * @return array|null 328 | */ 329 | private static function languageSpecificCharsArray($language) 330 | { 331 | static $languageSpecific; 332 | 333 | if (! isset($languageSpecific)) { 334 | $languageSpecific = [ 335 | 'bg' => [ 336 | ['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'], 337 | ['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'], 338 | ], 339 | 'de' => [ 340 | ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü'], 341 | ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], 342 | ], 343 | ]; 344 | } 345 | 346 | return $languageSpecific[$language] ?? null; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/PathResolvers/PrettyOutputPathResolver.php: -------------------------------------------------------------------------------- 1 | 1) { 11 | return '/' . leftTrimPath(trimPath($path) . '/') . trimPath($prefix . '/' . $page) . '/'; 12 | } 13 | 14 | return leftTrimPath('/' . trimPath($path) . '/') . '/'; 15 | } 16 | 17 | if ($type === 'html' && $name !== 'index') { 18 | if ($page > 1) { 19 | return '/' . leftTrimPath(trimPath($path) . '/') . $name . '/' . trimPath($prefix . '/' . $page) . '/'; 20 | } 21 | 22 | return '/' . leftTrimPath(trimPath($path) . '/') . $name . '/'; 23 | } 24 | 25 | return sprintf('%s%s%s.%s', '/', leftTrimPath(trimPath($path) . '/'), $name, $type); 26 | } 27 | 28 | public function path($path, $name, $type, $page = 1, $prefix = '') 29 | { 30 | if ($type === 'html' && $name === 'index' && $page > 1) { 31 | return leftTrimPath(trimPath($path) . '/' . trimPath($prefix . '/' . $page) . '/index.html'); 32 | } 33 | 34 | if ($type === 'html' && $name !== 'index') { 35 | if ($page > 1) { 36 | return trimPath($path) . '/' . $name . '/' . trimPath($prefix . '/' . $page) . '/index.html'; 37 | } 38 | 39 | return trimPath($path) . '/' . $name . '/index.html'; 40 | } 41 | 42 | if (empty($type)) { 43 | return sprintf('%s%s%s', trimPath($path), '/', $name); 44 | } 45 | 46 | return sprintf('%s%s%s.%s', trimPath($path), '/', $name, $type); 47 | } 48 | 49 | public function directory($path, $name, $type, $page = 1, $prefix = '') 50 | { 51 | if ($type === 'html' && $name === 'index' && $page > 1) { 52 | return leftTrimPath(trimPath($path) . '/' . trimPath($prefix . '/' . $page)); 53 | } 54 | 55 | if ($type === 'html' && $name !== 'index') { 56 | if ($page > 1) { 57 | return trimPath($path) . '/' . $name . '/' . trimPath($prefix . '/' . $page); 58 | } 59 | 60 | return trimPath($path) . '/' . $name; 61 | } 62 | 63 | return trimPath($path); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Providers/BootstrapFileServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->path('bootstrap.php'))) { 12 | $events = $this->app->events; 13 | $container = $this->app; 14 | $cachePath = $this->app->cachePath(); 15 | $envPath = $this->app->path('.env'); 16 | $bladeCompiler = $this->app['blade.compiler']; 17 | 18 | include $bootstrapFile; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Providers/CollectionServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('outputPathResolver', fn () => new BasicOutputPathResolver); 32 | 33 | $this->registerHandlers(); 34 | $this->registerPathResolver(); 35 | $this->registerLoaders(); 36 | $this->registerPaginator(); 37 | $this->registerSiteBuilder(); 38 | 39 | $this->app->bind(Jigsaw::class, function (Container $app) { 40 | return new Jigsaw($app, $app[DataLoader::class], $app[CollectionRemoteItemLoader::class], $app[SiteBuilder::class]); 41 | }); 42 | } 43 | 44 | private function registerHandlers(): void 45 | { 46 | $this->app->bind(BladeHandler::class, function (Container $app) { 47 | return new BladeHandler($app[TemporaryFilesystem::class], $app[FrontMatterParser::class], $app[ViewRenderer::class]); 48 | }); 49 | 50 | $this->app->bind(MarkdownHandler::class, function (Container $app) { 51 | return new MarkdownHandler($app[TemporaryFilesystem::class], $app[FrontMatterParser::class], $app[ViewRenderer::class]); 52 | }); 53 | 54 | $this->app->bind(CollectionItemHandler::class, function (Container $app) { 55 | return new CollectionItemHandler($app['config'], [ 56 | $app[MarkdownHandler::class], 57 | $app[BladeHandler::class], 58 | ]); 59 | }); 60 | } 61 | 62 | private function registerPathResolver(): void 63 | { 64 | $this->app->bind(CollectionPathResolver::class, function (Container $app) { 65 | return new CollectionPathResolver($app['outputPathResolver'], $app[ViewRenderer::class]); 66 | }); 67 | } 68 | 69 | private function registerLoaders(): void 70 | { 71 | $this->app->bind(CollectionDataLoader::class, function (Container $app) { 72 | return new CollectionDataLoader($app['files'], $app['consoleOutput'], $app[CollectionPathResolver::class], [ 73 | $app[MarkdownCollectionItemHandler::class], 74 | $app[BladeCollectionItemHandler::class], 75 | ]); 76 | }); 77 | 78 | $this->app->bind(DataLoader::class, function (Container $app) { 79 | return new DataLoader($app[CollectionDataLoader::class]); 80 | }); 81 | 82 | $this->app->bind(CollectionRemoteItemLoader::class, function (Container $app) { 83 | return new CollectionRemoteItemLoader($app['config'], $app['files']); 84 | }); 85 | } 86 | 87 | private function registerPaginator(): void 88 | { 89 | $this->app->bind(CollectionPaginator::class, function (Container $app) { 90 | return new CollectionPaginator($app['outputPathResolver']); 91 | }); 92 | 93 | $this->app->bind(PaginatedPageHandler::class, function (Container $app) { 94 | return new PaginatedPageHandler($app[CollectionPaginator::class], $app[FrontMatterParser::class], $app[TemporaryFilesystem::class], $app[ViewRenderer::class]); 95 | }); 96 | } 97 | 98 | private function registerSiteBuilder(): void 99 | { 100 | $this->app->bind(SiteBuilder::class, function (Container $app) { 101 | return new SiteBuilder($app['files'], $app->cachePath(), $app['outputPathResolver'], $app['consoleOutput'], [ 102 | $app[CollectionItemHandler::class], 103 | new IgnoredHandler, 104 | $app[PaginatedPageHandler::class], 105 | $app[MarkdownHandler::class], 106 | $app[BladeHandler::class], 107 | $app[DefaultHandler::class], 108 | ]); 109 | }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Providers/CompatibilityServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->instance('cwd', $this->app->path()); 13 | 14 | $this->app->singleton('consoleOutput', fn () => new ConsoleOutput); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('dispatcher', fn (Container $app) => new Dispatcher($app)); 15 | 16 | $this->app->singleton('events', fn (Container $app) => new EventBus); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Providers/ExceptionServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make(ExceptionHandler::class)->map( 15 | fn (ViewException $e) => $this->app->make(ViewExceptionMapper::class)->map($e), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Providers/FilesystemServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('files', fn () => new Filesystem); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Providers/MarkdownServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(YAMLParser::class, SymfonyYAMLParser::class); 22 | 23 | $this->app->bind(MarkdownParserContract::class, function (Container $app) { 24 | return $app['config']->get('commonmark') ? new CommonMarkParser : new JigsawMarkdownParser; 25 | }); 26 | 27 | $this->app->singleton('markdownParser', fn (Container $app) => new MarkdownParser($app[MarkdownParserContract::class])); 28 | 29 | // Make the FrontYAML package use our own Markdown parser internally 30 | $this->app->bind(FrontYAMLMarkdownParser::class, fn (Container $app) => $app['markdownParser']); 31 | 32 | $this->app->bind(Parser::class, function (Container $app) { 33 | return new Parser($app[YAMLParser::class], $app[FrontYAMLMarkdownParser::class]); 34 | }); 35 | 36 | $this->app->bind(FrontMatterParser::class, function (Container $app) { 37 | return new FrontMatterParser($app[Parser::class]); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Providers/ViewServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerFactory(); 27 | $this->registerViewFinder(); 28 | $this->registerBladeCompiler(); 29 | $this->registerEngineResolvers(); 30 | 31 | (new BladeDirectivesFile($this->app->path('blade.php'), $this->app['blade.compiler']))->register(); 32 | $this->app->bind(ViewRenderer::class, fn () => new ViewRenderer); 33 | $this->app->bind(TemporaryFilesystem::class, fn (Container $app) => new TemporaryFilesystem($app->cachePath())); 34 | 35 | // TODO 36 | // $this->registerExtensionEngines(); 37 | // $this->registerConfiguredHintPaths(); 38 | } 39 | 40 | private function registerFactory(): void 41 | { 42 | $this->app->singleton('view', function (Container $app) { 43 | $factory = new Factory($app['view.engine.resolver'], $app['view.finder'], $app['dispatcher']); 44 | 45 | $factory->setContainer($app); 46 | // TODO provide a magic `$app` variable to all views? 47 | // $factory->share('app', $app); 48 | 49 | return $factory; 50 | }); 51 | } 52 | 53 | private function registerViewFinder(): void 54 | { 55 | $this->app->bind('view.finder', function (Container $app) { 56 | // TODO $app['config']['view.paths'] 57 | return new FileViewFinder($app['files'], [$app->cachePath(), $app['buildPath']['views']]); 58 | }); 59 | } 60 | 61 | private function registerBladeCompiler(): void 62 | { 63 | $this->app->singleton('blade.compiler', function (Container $app) { 64 | // TODO $app['config']['view.compiled'] 65 | return tap(new BladeCompiler($app['files'], $app->cachePath()), function ($blade) { 66 | $blade->component('dynamic-component', DynamicComponent::class); 67 | }); 68 | }); 69 | 70 | // v1 binding is 'bladeCompiler' 71 | $this->app->alias('blade.compiler', 'bladeCompiler'); 72 | } 73 | 74 | private function registerEngineResolvers(): void 75 | { 76 | $this->app->singleton('view.engine.resolver', function (Container $app) { 77 | $resolver = new EngineResolver; 78 | $compilerEngine = new CompilerEngine($app['blade.compiler'], $app['files']); 79 | 80 | // Same as Laravel 81 | $resolver->register('file', fn () => new FileEngine($app['files'])); 82 | $resolver->register('php', fn () => new PhpEngine($app['files'])); 83 | $resolver->register('blade', fn () => $compilerEngine); 84 | 85 | // Specific to Jigsaw 86 | // TODO $app['config']['view.paths'] 87 | $resolver->register('markdown', fn () => new MarkdownEngine($app[FrontMatterParser::class], $app['files'], $app['buildPath']['views'])); 88 | $resolver->register('blade-markdown', fn () => new BladeMarkdownEngine($compilerEngine, $this->app[FrontMatterParser::class])); 89 | 90 | return $resolver; 91 | }); 92 | } 93 | 94 | private function registerExtensionEngines(): void 95 | { 96 | foreach (['md', 'markdown', 'mdown'] as $extension) { 97 | $this->app['view']->addExtension($extension, 'markdown'); 98 | $this->app['view']->addExtension("blade.{$extension}", 'blade-markdown'); 99 | } 100 | 101 | foreach (['js', 'json', 'xml', 'yaml', 'yml', 'rss', 'atom', 'txt', 'text', 'html'] as $extension) { 102 | $this->app['view']->addExtension($extension, 'php'); 103 | $this->app['view']->addExtension("blade.{$extension}", 'blade'); 104 | } 105 | } 106 | 107 | private function registerConfiguredHintPaths(): void 108 | { 109 | foreach ($this->app['config']->get('viewHintPaths', []) as $hint => $path) { 110 | $this->app['view']->addNamespace($hint, $path); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Scaffold/BasicScaffoldBuilder.php: -------------------------------------------------------------------------------- 1 | files->copyDirectory(__DIR__ . '/../../stubs/site', $this->base); 15 | 16 | return $this; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Scaffold/CustomInstaller.php: -------------------------------------------------------------------------------- 1 | console = $console; 20 | 21 | return $this; 22 | } 23 | 24 | public function install(ScaffoldBuilder $builder) 25 | { 26 | $this->builder = $builder; 27 | 28 | return $this; 29 | } 30 | 31 | public function setup() 32 | { 33 | $this->builder->buildBasicScaffold(); 34 | 35 | return $this; 36 | } 37 | 38 | public function copy($files = null) 39 | { 40 | $this->builder->cacheComposerDotJson(); 41 | $this->builder->copyPresetFiles($files, $this->ignore, $this->from); 42 | $this->builder->mergeComposerDotJson(); 43 | 44 | return $this; 45 | } 46 | 47 | public function from($from = null) 48 | { 49 | $this->from = $from; 50 | 51 | return $this; 52 | } 53 | 54 | public function ignore($files) 55 | { 56 | $this->ignore = array_merge($this->ignore, collect($files)->toArray()); 57 | 58 | return $this; 59 | } 60 | 61 | public function delete($files = null) 62 | { 63 | $this->builder->cacheComposerDotJson(); 64 | $this->builder->deleteSiteFiles($files); 65 | $this->builder->mergeComposerDotJson(); 66 | 67 | return $this; 68 | } 69 | 70 | public function run($commands = null) 71 | { 72 | $this->builder->runCommands($commands); 73 | 74 | return $this; 75 | } 76 | 77 | public function ask($question, $default = null, $options = null, $errorMessage = null) 78 | { 79 | return $this->console->ask($question, $default, $options, $errorMessage); 80 | } 81 | 82 | public function confirm($question, $default = null, $errorMessage = null) 83 | { 84 | return $this->console->confirm($question, $default); 85 | } 86 | 87 | public function output($text = '') 88 | { 89 | $this->console->write($text); 90 | 91 | return $this; 92 | } 93 | 94 | public function info($text = '') 95 | { 96 | $this->console->info($text); 97 | 98 | return $this; 99 | } 100 | 101 | public function error($text = '') 102 | { 103 | $this->console->error($text); 104 | 105 | return $this; 106 | } 107 | 108 | public function comment($text = '') 109 | { 110 | $this->console->comment($text); 111 | 112 | return $this; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Scaffold/DefaultInstaller.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 33 | $this->delete = Arr::get($settings, 'delete', []); 34 | $this->ignore = array_merge(self::ALWAYS_IGNORE, Arr::get($settings, 'ignore', [])); 35 | $commands = Arr::get($settings, 'commands'); 36 | $this->commands = $commands !== null ? $commands : self::DEFAULT_COMMANDS; 37 | $this->execute(); 38 | } 39 | 40 | public function execute() 41 | { 42 | return $this->builder 43 | ->buildBasicScaffold() 44 | ->cacheComposerDotJson() 45 | ->deleteSiteFiles($this->delete) 46 | ->copyPresetFiles(null, $this->ignore) 47 | ->mergeComposerDotJson() 48 | ->runCommands($this->commands); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Scaffold/InstallerCommandException.php: -------------------------------------------------------------------------------- 1 | 'tightenco/jigsaw-blog-template', 15 | 'docs' => 'tightenco/jigsaw-docs-template', 16 | ]; 17 | 18 | public $constraint; 19 | 20 | public $name; 21 | 22 | public $shortName; 23 | 24 | public $path; 25 | 26 | public $preset; 27 | 28 | public $suffix; 29 | 30 | public $vendor; 31 | 32 | protected $builder; 33 | 34 | protected $customInstaller; 35 | 36 | protected $defaultInstaller; 37 | 38 | protected $files; 39 | 40 | protected $process; 41 | 42 | public function __construct(DefaultInstaller $default, CustomInstaller $custom, ProcessRunner $process) 43 | { 44 | $this->defaultInstaller = $default; 45 | $this->customInstaller = $custom; 46 | $this->process = $process; 47 | $this->files = new Filesystem; 48 | } 49 | 50 | public function init($preset, PresetScaffoldBuilder $builder) 51 | { 52 | $this->preset = $preset; 53 | $this->builder = $builder; 54 | $this->resolveNames(); 55 | $this->resolvePath(); 56 | } 57 | 58 | public function runInstaller($console) 59 | { 60 | if (! $this->files->exists($this->path . DIRECTORY_SEPARATOR . 'init.php')) { 61 | return $this->runDefaultInstaller(); 62 | } 63 | 64 | try { 65 | $init = $this->customInstaller->setConsole($console)->install($this->builder); 66 | $initFile = include $this->path . DIRECTORY_SEPARATOR . 'init.php'; 67 | 68 | if (is_array($initFile) && count($initFile)) { 69 | return $this->runDefaultInstaller($initFile); 70 | } 71 | } catch (InstallerCommandException $e) { 72 | throw $e; 73 | } catch (Exception $e) { 74 | throw new Exception("The 'init.php' file for this preset contains errors."); 75 | } catch (Error $e) { 76 | throw new Exception("The 'init.php' file for this preset contains errors."); 77 | } 78 | } 79 | 80 | protected function runDefaultInstaller($settings = []) 81 | { 82 | $this->defaultInstaller->install($this->builder, $settings); 83 | } 84 | 85 | protected function resolveNames() 86 | { 87 | $name = Arr::get(self::PRESETS, $this->preset, $this->preset); 88 | 89 | if (! Str::contains($name, '/')) { 90 | throw new Exception("'{$name}' is not a valid package name."); 91 | } 92 | 93 | $parts = explode('/', $name, 3); 94 | $this->vendor = Arr::get($parts, 0); 95 | $composerName = explode(':', Arr::get($parts, 1)); 96 | $this->name = $composerName[0]; 97 | $this->constraint = $composerName[1] ?? ''; 98 | $this->suffix = Arr::get($parts, 2); 99 | $this->shortName = $this->getShortName(); 100 | } 101 | 102 | protected function getShortName() 103 | { 104 | return Str::contains($this->preset, '/') ? 105 | explode('/', $this->preset)[1] : 106 | $this->preset; 107 | } 108 | 109 | protected function resolvePath() 110 | { 111 | $this->path = collect([$this->builder->base, 'vendor', $this->vendor, $this->name, $this->suffix]) 112 | ->filter() 113 | ->implode(DIRECTORY_SEPARATOR); 114 | 115 | if (! $this->files->exists($this->path)) { 116 | $package = $this->vendor . '/' . $this->name; 117 | 118 | if (! empty($this->constraint)) { 119 | $package .= ':' . $this->constraint; 120 | } 121 | 122 | try { 123 | $this->installPackageFromComposer($package); 124 | } catch (Exception $e) { 125 | throw new Exception("The '{$package}' preset could not be found."); 126 | } 127 | } 128 | } 129 | 130 | protected function installPackageFromComposer($package) 131 | { 132 | $this->process->run('composer require ' . $package); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Scaffold/PresetScaffoldBuilder.php: -------------------------------------------------------------------------------- 1 | package = $package; 25 | $this->process = $process; 26 | } 27 | 28 | public function init($preset) 29 | { 30 | $this->package->init($preset, $this); 31 | $this->addPackageToCachedComposerRequires(); 32 | 33 | return $this; 34 | } 35 | 36 | public function build() 37 | { 38 | $this->package->runInstaller($this->console); 39 | 40 | return $this; 41 | } 42 | 43 | public function buildBasicScaffold() 44 | { 45 | (new BasicScaffoldBuilder($this->files)) 46 | ->setBase($this->base) 47 | ->build(); 48 | 49 | return $this; 50 | } 51 | 52 | public function mergeComposerDotJson() 53 | { 54 | $newComposer = collect($this->getComposer()) 55 | ->forget(['name', 'type', 'version', 'description', 'keywords', 'license', 'authors']) 56 | ->toArray() 57 | ?? []; 58 | $merged = array_merge_recursive($this->composerCache ?? [], $newComposer); 59 | $this->writeComposer($this->preferVersionConstraintFromCached($merged)); 60 | 61 | return $this; 62 | } 63 | 64 | public function deleteSiteFiles($match = []) 65 | { 66 | if (collect($match)->count()) { 67 | collect($this->getSiteFilesAndDirectories($match)) 68 | ->each(function ($file) { 69 | $source = $file->getPathName(); 70 | 71 | if ($this->files->isDirectory($file)) { 72 | $this->files->deleteDirectory($source); 73 | } elseif ($this->files->isFile($file)) { 74 | $this->files->delete($source); 75 | } 76 | }); 77 | } 78 | 79 | return $this; 80 | } 81 | 82 | public function copyPresetFiles($match = [], $ignore = [], $directory = null) 83 | { 84 | $source = $this->package->path . 85 | ($directory ? DIRECTORY_SEPARATOR . trim($directory, '/') : ''); 86 | 87 | collect($this->getPresetDirectories($source, $match, $ignore)) 88 | ->each(function ($directory) { 89 | $destination = $this->base . DIRECTORY_SEPARATOR . $directory->getRelativePathName(); 90 | 91 | if (! $this->files->exists($destination)) { 92 | $this->files->makeDirectory($destination, 0755, true); 93 | } 94 | }); 95 | 96 | collect($this->getPresetFiles($source, $match, $ignore)) 97 | ->each(function ($file) { 98 | $source = $file->getPathName(); 99 | $destination = $this->base . DIRECTORY_SEPARATOR . $file->getRelativePathName(); 100 | $this->files->copy($source, $destination); 101 | }); 102 | 103 | return $this; 104 | } 105 | 106 | public function runCommands($commands = []) 107 | { 108 | $this->process->run($commands); 109 | 110 | return $this; 111 | } 112 | 113 | protected function addPackageToCachedComposerRequires() 114 | { 115 | $this->composerDependencies[] = $this->package->vendor . DIRECTORY_SEPARATOR . $this->package->name; 116 | } 117 | 118 | protected function createDirectoryForFile($file) 119 | { 120 | $path = $file->getRelativePath(); 121 | 122 | if ($path && ! $this->files->isDirectory($path)) { 123 | $this->files->makeDirectory($path, 0755, true); 124 | } 125 | } 126 | 127 | protected function getSiteFilesAndDirectories($match = [], $ignore = []) 128 | { 129 | return $this->files->filesAndDirectories($this->base, $match, $ignore); 130 | } 131 | 132 | protected function getPresetDirectories($source, $match = [], $ignore = []) 133 | { 134 | return $this->files->directories($source, $match, $ignore); 135 | } 136 | 137 | protected function getPresetFiles($source, $match = [], $ignore = []) 138 | { 139 | return $this->files->files($source, $match, $ignore); 140 | } 141 | 142 | protected function preferVersionConstraintFromCached($composer) 143 | { 144 | $require = collect(Arr::get($composer, 'require'))->mapWithKeys(function ($version, $package) { 145 | return [$package => is_array($version) ? $version[0] : $version]; 146 | }); 147 | 148 | return Arr::set($composer, 'require', $require); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Scaffold/ProcessRunner.php: -------------------------------------------------------------------------------- 1 | each(function ($command) { 13 | $this->runCommand($command); 14 | }); 15 | 16 | if ($commands) { 17 | echo "\n"; 18 | } 19 | 20 | return $this; 21 | } 22 | 23 | protected function runCommand($command) 24 | { 25 | echo "\n> " . $command . "\n"; 26 | $process = Process::fromShellCommandline($command); 27 | $process->setTimeout(3600); 28 | $process->setIdleTimeout(120); 29 | 30 | try { 31 | $process->setTty(true)->run(); 32 | } catch (RuntimeException $e) { 33 | $process->run(function ($type, $buffer) { 34 | echo $buffer; 35 | }); 36 | } 37 | 38 | if (! $process->isSuccessful()) { 39 | throw new InstallerCommandException($command); 40 | } 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Scaffold/ScaffoldBuilder.php: -------------------------------------------------------------------------------- 1 | files = $files; 28 | $this->setBase(); 29 | } 30 | 31 | abstract public function init($preset); 32 | 33 | abstract public function build(); 34 | 35 | public function setBase($cwd = null) 36 | { 37 | $this->base = $cwd ?: getcwd(); 38 | 39 | return $this; 40 | } 41 | 42 | public function setConsole($console) 43 | { 44 | $this->console = $console; 45 | 46 | return $this; 47 | } 48 | 49 | public function archiveExistingSite() 50 | { 51 | $this->cacheComposerDotJson(); 52 | $this->createEmptyArchive(); 53 | 54 | collect($this->allBaseFiles())->each(function ($file) use (&$directories) { 55 | $source = $file->getPathName(); 56 | $destination = $this->base . DIRECTORY_SEPARATOR . 'archived' . DIRECTORY_SEPARATOR . $file->getRelativePathName(); 57 | 58 | if ($this->files->isDirectory($file)) { 59 | $directories[] = $source; 60 | $this->files->makeDirectory($destination, 0755, true); 61 | } else { 62 | $this->files->move($source, $destination); 63 | } 64 | }); 65 | 66 | $this->deleteEmptyDirectories($directories); 67 | $this->restoreComposerDotJson(); 68 | } 69 | 70 | public function deleteExistingSite() 71 | { 72 | $this->cacheComposerDotJson(); 73 | 74 | collect($this->allBaseFiles())->each(function ($file) use (&$directories) { 75 | $source = $file->getPathName(); 76 | 77 | if ($this->files->isDirectory($file)) { 78 | $directories[] = $source; 79 | } else { 80 | $this->files->delete($source); 81 | } 82 | }); 83 | 84 | $this->deleteEmptyDirectories($directories); 85 | $this->restoreComposerDotJson(); 86 | } 87 | 88 | public function cacheComposerDotJson() 89 | { 90 | $this->composerCache = $this->getComposer() ?? []; 91 | 92 | return $this; 93 | } 94 | 95 | public function restoreComposerDotJson() 96 | { 97 | $composer = collect($this->composerCache)->only(['require', 'repositories']); 98 | 99 | if ($composer->count() && $jigsaw_require = collect($composer->get('require'))->only('tightenco/jigsaw')) { 100 | $this->writeComposer($composer->put('require', $jigsaw_require)); 101 | } 102 | } 103 | 104 | protected function createEmptyArchive() 105 | { 106 | $archived = $this->base . DIRECTORY_SEPARATOR . 'archived'; 107 | $this->files->deleteDirectory($archived); 108 | $this->files->makeDirectory($archived, 0755, true); 109 | } 110 | 111 | protected function deleteEmptyDirectories($directories) 112 | { 113 | collect($directories)->each(function ($directory) { 114 | if ($this->files->isEmptyDirectory($directory)) { 115 | $this->files->deleteDirectory($directory); 116 | } 117 | }); 118 | } 119 | 120 | protected function allBaseFiles() 121 | { 122 | return $this->files->filesAndDirectories( 123 | $this->base, 124 | null, 125 | self::IGNORE_DIRECTORIES, 126 | $ignore_dotfiles = false, 127 | ); 128 | } 129 | 130 | protected function getComposer() 131 | { 132 | $composer = $this->base . DIRECTORY_SEPARATOR . 'composer.json'; 133 | 134 | if ($this->files->exists($composer)) { 135 | return json_decode($this->files->get($composer), true); 136 | } 137 | } 138 | 139 | protected function writeComposer($content = null) 140 | { 141 | if ($content) { 142 | $this->files->put( 143 | $this->base . DIRECTORY_SEPARATOR . 'composer.json', 144 | json_encode($content, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), 145 | ); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/SiteBuilder.php: -------------------------------------------------------------------------------- 1 | files = $files; 32 | $this->cachePath = $cachePath; 33 | $this->outputPathResolver = $outputPathResolver; 34 | $this->consoleOutput = $consoleOutput; 35 | $this->handlers = $handlers; 36 | } 37 | 38 | public function setUseCache($useCache) 39 | { 40 | $this->useCache = $useCache; 41 | 42 | return $this; 43 | } 44 | 45 | public function build($source, $destination, $siteData) 46 | { 47 | $this->prepareDirectory($this->cachePath, ! $this->useCache); 48 | $generatedFiles = $this->generateFiles($source, $siteData); 49 | $this->prepareDirectory($destination, true); 50 | $outputFiles = $this->writeFiles($generatedFiles, $destination); 51 | $this->cleanup(); 52 | 53 | return $outputFiles; 54 | } 55 | 56 | public function registerHandler($handler) 57 | { 58 | $this->handlers[] = $handler; 59 | } 60 | 61 | private function prepareDirectories($directories) 62 | { 63 | foreach ($directories as $directory) { 64 | $this->prepareDirectory($directory, true); 65 | } 66 | } 67 | 68 | private function prepareDirectory($directory, $clean = false) 69 | { 70 | if (! $this->files->isDirectory($directory)) { 71 | $this->files->makeDirectory($directory, 0755, true); 72 | } 73 | 74 | if ($clean) { 75 | $this->files->cleanDirectory($directory); 76 | } 77 | } 78 | 79 | private function cleanup() 80 | { 81 | if (! $this->useCache) { 82 | $this->files->deleteDirectory($this->cachePath); 83 | } 84 | } 85 | 86 | private function generateFiles($source, $siteData) 87 | { 88 | $files = collect($this->files->files($source)); 89 | $this->consoleOutput->startProgressBar('build', $files->count()); 90 | 91 | $files = $files->map(function ($file) { 92 | return new InputFile($file); 93 | })->flatMap(function ($file) use ($siteData) { 94 | $this->consoleOutput->progressBar('build')->advance(); 95 | 96 | return $this->handle($file, $siteData); 97 | }); 98 | 99 | return $files; 100 | } 101 | 102 | private function writeFiles($files, $destination) 103 | { 104 | $this->consoleOutput->writeWritingFiles(); 105 | 106 | return $files->mapWithKeys(function ($file) use ($destination) { 107 | $outputLink = $this->writeFile($file, $destination); 108 | 109 | return [$outputLink => $file->inputFile()->getPageData()]; 110 | }); 111 | } 112 | 113 | private function writeFile($file, $destination) 114 | { 115 | $directory = $this->getOutputDirectory($file); 116 | $this->prepareDirectory("{$destination}/{$directory}"); 117 | $file->putContents("{$destination}/{$this->getOutputPath($file)}"); 118 | 119 | return $this->getOutputLink($file); 120 | } 121 | 122 | private function handle($file, $siteData) 123 | { 124 | $meta = $this->getMetaData($file, $siteData->page->baseUrl); 125 | 126 | $pageData = PageData::withPageMetaData($siteData, $meta); 127 | Container::getInstance()->instance('pageData', $pageData); 128 | 129 | return $this->getHandler($file)->handle($file, $pageData); 130 | } 131 | 132 | private function getHandler($file) 133 | { 134 | return collect($this->handlers)->first(function ($handler) use ($file) { 135 | return $handler->shouldHandle($file); 136 | }); 137 | } 138 | 139 | private function getMetaData($file, $baseUrl) 140 | { 141 | $filename = $file->getFilenameWithoutExtension(); 142 | $extension = $file->getFullExtension(); 143 | $path = rightTrimPath($this->outputPathResolver->link($file->getRelativePath(), $filename, $file->getExtraBladeExtension() ?: 'html')); 144 | $relativePath = $file->getRelativePath(); 145 | $url = rightTrimPath($baseUrl) . '/' . trimPath($path); 146 | $modifiedTime = $file->getLastModifiedTime(); 147 | 148 | return compact('filename', 'baseUrl', 'path', 'relativePath', 'extension', 'url', 'modifiedTime'); 149 | } 150 | 151 | private function getOutputDirectory($file) 152 | { 153 | if ($permalink = $this->getFilePermalink($file)) { 154 | return urldecode(dirname($permalink)); 155 | } 156 | 157 | return urldecode($this->outputPathResolver->directory($file->path(), $file->name(), $file->extension(), $file->page(), $file->prefix())); 158 | } 159 | 160 | private function getOutputPath($file) 161 | { 162 | if ($permalink = $this->getFilePermalink($file)) { 163 | return $permalink; 164 | } 165 | 166 | return resolvePath(urldecode($this->outputPathResolver->path( 167 | $file->path(), 168 | $file->name(), 169 | $file->extension(), 170 | $file->page(), 171 | $file->prefix(), 172 | ))); 173 | } 174 | 175 | private function getOutputLink($file) 176 | { 177 | if ($permalink = $this->getFilePermalink($file)) { 178 | return $permalink; 179 | } 180 | 181 | return rightTrimPath(urldecode($this->outputPathResolver->link( 182 | str_replace('\\', '/', $file->path()), 183 | $file->name(), 184 | $file->extension(), 185 | $file->page(), 186 | ))); 187 | } 188 | 189 | private function getFilePermalink($file) 190 | { 191 | return $file->data()->page->permalink ? '/' . resolvePath(urldecode($file->data()->page->permalink)) : null; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/SiteData.php: -------------------------------------------------------------------------------- 1 | putIterable('collections', $config->get('collections')); 13 | $siteData->putIterable('page', $config); 14 | 15 | return $siteData; 16 | } 17 | 18 | public function addCollectionData(array $collectionData) 19 | { 20 | collect($collectionData)->each(function ($collection, $collectionName) { 21 | return $this->put($collectionName, new PageVariable($collection)); 22 | }); 23 | 24 | return $this->forget('collections'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Support/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | make($abstract, $parameters); 17 | } 18 | } 19 | 20 | function leftTrimPath($path) 21 | { 22 | return ltrim($path, ' \\/'); 23 | } 24 | 25 | function rightTrimPath($path) 26 | { 27 | return rtrim($path ?? '', ' .\\/'); 28 | } 29 | 30 | function trimPath($path) 31 | { 32 | return rightTrimPath(leftTrimPath($path)); 33 | } 34 | 35 | function resolvePath($path) 36 | { 37 | $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); 38 | $segments = []; 39 | 40 | collect(explode(DIRECTORY_SEPARATOR, $path)) 41 | ->filter(fn ($part) => (string) $part !== '') 42 | ->each(function ($part) use (&$segments) { 43 | if ($part == '..') { 44 | array_pop($segments); 45 | } elseif ($part != '.') { 46 | $segments[] = $part; 47 | } 48 | }); 49 | 50 | return implode(DIRECTORY_SEPARATOR, $segments); 51 | } 52 | 53 | /** 54 | * Get the path to the public folder. 55 | */ 56 | function public_path($path = '') 57 | { 58 | $c = Container::getInstance(); 59 | $source = Arr::get($c['config'], 'build.source', 'source'); 60 | 61 | return $source . ($path ? '/' . ltrim($path, '/') : $path); 62 | } 63 | 64 | /** 65 | * Get the path to a versioned Elixir file. 66 | */ 67 | function elixir($file, $buildDirectory = 'build') 68 | { 69 | static $manifest; 70 | static $manifestPath; 71 | 72 | if (is_null($manifest) || $manifestPath !== $buildDirectory) { 73 | $manifest = json_decode(file_get_contents(public_path($buildDirectory . '/rev-manifest.json')), true); 74 | 75 | $manifestPath = $buildDirectory; 76 | } 77 | 78 | if (isset($manifest[$file])) { 79 | return '/' . trim($buildDirectory . '/' . $manifest[$file], '/'); 80 | } 81 | 82 | throw new InvalidArgumentException("File {$file} not defined in asset manifest."); 83 | } 84 | 85 | /** 86 | * Get the path to a versioned Mix file. 87 | */ 88 | function mix($path, $manifestDirectory = 'assets') 89 | { 90 | static $manifests = []; 91 | 92 | if (! Str::startsWith($path, '/')) { 93 | $path = "/{$path}"; 94 | } 95 | 96 | if ($manifestDirectory && ! Str::startsWith($manifestDirectory, '/')) { 97 | $manifestDirectory = "/{$manifestDirectory}"; 98 | } 99 | 100 | if (file_exists(public_path($manifestDirectory . '/hot'))) { 101 | return new HtmlString("//localhost:8080{$path}"); 102 | } 103 | 104 | $manifestPath = public_path($manifestDirectory . '/mix-manifest.json'); 105 | 106 | if (! isset($manifests[$manifestPath])) { 107 | if (! file_exists($manifestPath)) { 108 | throw new Exception('The Mix manifest does not exist.'); 109 | } 110 | 111 | $manifests[$manifestPath] = json_decode(file_get_contents($manifestPath), true); 112 | } 113 | 114 | $manifest = $manifests[$manifestPath]; 115 | 116 | if (! isset($manifest[$path])) { 117 | throw new InvalidArgumentException("Unable to locate Mix file: {$path}."); 118 | } 119 | 120 | return new HtmlString($manifestDirectory . $manifest[$path]); 121 | } 122 | 123 | if (! function_exists('url')) { 124 | function url(string $path): string 125 | { 126 | $c = Container::getInstance(); 127 | 128 | return trim($c['config']['baseUrl'], '/') . '/' . trim($path, '/'); 129 | } 130 | } 131 | 132 | if (! function_exists('dd')) { 133 | function dd(...$args) 134 | { 135 | foreach ($args as $x) { 136 | (new VarDumper)->dump($x); 137 | } 138 | 139 | exit(1); 140 | } 141 | } 142 | 143 | function inline($assetPath) 144 | { 145 | preg_match('/^\/assets\/build\/(css|js)\/.*\.(css|js)/', $assetPath, $matches); 146 | 147 | if (! count($matches)) { 148 | throw new InvalidArgumentException("Given asset path is not valid: {$assetPath}"); 149 | } 150 | 151 | $pathParts = explode('?', $assetPath); 152 | 153 | return new HtmlString(file_get_contents("source{$pathParts[0]}")); 154 | } 155 | -------------------------------------------------------------------------------- /src/View/BladeCompiler.php: -------------------------------------------------------------------------------- 1 | compilesComponentTags) { 18 | return $value; 19 | } 20 | 21 | return (new ComponentTagCompiler( 22 | $this->classComponentAliases, $this->classComponentNamespaces, $this, 23 | ))->compile($value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/View/BladeMarkdownEngine.php: -------------------------------------------------------------------------------- 1 | blade = $compilerEngine; 19 | $this->markdown = $markdown; 20 | } 21 | 22 | public function get($path, array $data = []) 23 | { 24 | $content = $this->evaluateBlade($path, $data); 25 | 26 | return $this->evaluateMarkdown($content); 27 | } 28 | 29 | protected function evaluateBlade($path, $data) 30 | { 31 | try { 32 | return $this->blade->get($path, $data); 33 | } catch (Exception $e) { 34 | $this->handleViewException($e); 35 | } catch (Throwable $e) { 36 | $this->handleViewException(new FatalThrowableError($e)); 37 | } 38 | } 39 | 40 | protected function evaluateMarkdown($content) 41 | { 42 | try { 43 | return $this->markdown->parseMarkdown($content); 44 | } catch (Exception $e) { 45 | $this->handleViewException($e); 46 | } catch (Throwable $e) { 47 | $this->handleViewException(new FatalThrowableError($e)); 48 | } 49 | } 50 | 51 | protected function handleViewException(Exception $e) 52 | { 53 | throw $e; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/View/ComponentTagCompiler.php: -------------------------------------------------------------------------------- 1 | make(Factory::class); 23 | 24 | if (isset($this->aliases[$component])) { 25 | if (class_exists($alias = $this->aliases[$component])) { 26 | return $alias; 27 | } 28 | 29 | if ($viewFactory->exists($alias)) { 30 | return $alias; 31 | } 32 | 33 | throw new InvalidArgumentException("Unable to locate class or view [{$alias}] for component [{$component}]."); 34 | } 35 | 36 | if ($class = $this->findClassByComponent($component)) { 37 | return $class; 38 | } 39 | 40 | if (class_exists($class = $this->guessClassName($component))) { 41 | return $class; 42 | } 43 | 44 | $guess = collect($this->blade->getAnonymousComponentNamespaces()) 45 | ->filter(function ($directory, $prefix) use ($component) { 46 | return Str::startsWith($component, $prefix . '::'); 47 | }) 48 | // This is the only line that differs from the overidden method (to add the leading underscore) 49 | ->prepend('_components', $component) 50 | ->reduce(function ($carry, $directory, $prefix) use ($component, $viewFactory) { 51 | if (! is_null($carry)) { 52 | return $carry; 53 | } 54 | 55 | $componentName = Str::after($component, $prefix . '::'); 56 | 57 | if ($viewFactory->exists($view = $this->guessViewName($componentName, $directory))) { 58 | return $view; 59 | } 60 | 61 | if ($viewFactory->exists($view = $this->guessViewName($componentName, $directory) . '.index')) { 62 | return $view; 63 | } 64 | }); 65 | 66 | if (! is_null($guess)) { 67 | return $guess; 68 | } 69 | 70 | throw new InvalidArgumentException("Unable to locate a class or view for component [{$component}]."); 71 | } 72 | 73 | /** 74 | * Guess the class name for the given component. 75 | * 76 | * @return string 77 | */ 78 | public function guessClassName(string $component) 79 | { 80 | return 'Components\\' . $this->formatClassName($component); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/View/DynamicComponent.php: -------------------------------------------------------------------------------- 1 | make('blade.compiler')->getClassComponentAliases(), 15 | Container::getInstance()->make('blade.compiler')->getClassComponentNamespaces(), 16 | Container::getInstance()->make('blade.compiler') 17 | ); 18 | } 19 | 20 | return static::$compiler; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/View/MarkdownEngine.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 21 | $this->file = $filesystem; 22 | $this->sourcePath = $sourcePath; 23 | } 24 | 25 | public function get($path, array $data = []) 26 | { 27 | return $this->evaluateMarkdown($path); 28 | } 29 | 30 | protected function evaluateMarkdown($path) 31 | { 32 | try { 33 | $file = $this->file->get($path); 34 | 35 | if ($file) { 36 | return $this->parser->parseMarkdown($file); 37 | } 38 | } catch (Exception $e) { 39 | $this->handleViewException($e); 40 | } catch (Throwable $e) { 41 | $this->handleViewException(new FatalThrowableError($e)); 42 | } 43 | } 44 | 45 | protected function handleViewException(Exception $e) 46 | { 47 | throw $e; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/View/ViewRenderer.php: -------------------------------------------------------------------------------- 1 | addExtensions(); 15 | $this->addHintpaths(); 16 | } 17 | 18 | public function getExtension($bladeViewPath) 19 | { 20 | return strtolower(pathinfo(app('view')->getFinder()->find($bladeViewPath), PATHINFO_EXTENSION)); 21 | } 22 | 23 | public function render($path, $data) 24 | { 25 | return app('view')->file($path, $data->all())->render(); 26 | } 27 | 28 | public function renderString($string) 29 | { 30 | return app('blade.compiler')->compileString($string); 31 | } 32 | 33 | private function addHintpaths() 34 | { 35 | foreach (app('config')->get('viewHintPaths', []) as $hint => $path) { 36 | app('view')->addNamespace($hint, $path); 37 | } 38 | } 39 | 40 | private function addExtensions() 41 | { 42 | foreach (['md', 'markdown', 'mdown'] as $extension) { 43 | app('view')->addExtension($extension, 'markdown'); 44 | app('view')->addExtension("blade.{$extension}", 'blade-markdown'); 45 | } 46 | 47 | foreach (['js', 'json', 'xml', 'yaml', 'yml', 'rss', 'atom', 'txt', 'text', 'html'] as $extension) { 48 | app('view')->addExtension($extension, 'php'); 49 | app('view')->addExtension("blade.{$extension}", 'blade'); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /stubs/site/.gitignore: -------------------------------------------------------------------------------- 1 | /build_local/ 2 | /cache/ 3 | /node_modules/ 4 | /vendor/ 5 | /.idea/ 6 | /.vscode/ 7 | npm-debug.log 8 | 9 | # Optional ignores 10 | # /build_staging/ 11 | # /build_production/ 12 | # /source/assets/build/ 13 | -------------------------------------------------------------------------------- /stubs/site/bootstrap.php: -------------------------------------------------------------------------------- 1 | beforeBuild(function (Jigsaw $jigsaw) { 15 | * // Your code here 16 | * }); 17 | */ 18 | -------------------------------------------------------------------------------- /stubs/site/config.php: -------------------------------------------------------------------------------- 1 | false, 5 | 'baseUrl' => '', 6 | 'title' => 'Jigsaw', 7 | 'description' => 'Website description.', 8 | 'collections' => [], 9 | ]; 10 | -------------------------------------------------------------------------------- /stubs/site/config.production.php: -------------------------------------------------------------------------------- 1 | true, 5 | ]; 6 | -------------------------------------------------------------------------------- /stubs/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "mix", 5 | "watch": "mix watch", 6 | "staging": "NODE_ENV=staging mix", 7 | "prod": "mix --production" 8 | }, 9 | "devDependencies": { 10 | "laravel-mix": "^6.0.39", 11 | "laravel-mix-jigsaw": "^2.0.0", 12 | "postcss": "^8.4.14", 13 | "postcss-import": "^14.0.0", 14 | "tailwindcss": "^3.1.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /stubs/site/source/_assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | -------------------------------------------------------------------------------- /stubs/site/source/_assets/js/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tighten/jigsaw/4ca1777989031ee628ad03fee3e44686e130a0ee/stubs/site/source/_assets/js/main.js -------------------------------------------------------------------------------- /stubs/site/source/_layouts/main.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ $page->title }} 9 | 10 | 11 | 12 | 13 | @yield('body') 14 | 15 | 16 | -------------------------------------------------------------------------------- /stubs/site/source/assets/build/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.1.6 | MIT License | https://tailwindcss.com 3 | *//* 4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 6 | */ 7 | 8 | *, 9 | ::before, 10 | ::after { 11 | box-sizing: border-box; /* 1 */ 12 | border-width: 0; /* 2 */ 13 | border-style: solid; /* 2 */ 14 | border-color: #e5e7eb; /* 2 */ 15 | } 16 | 17 | ::before, 18 | ::after { 19 | --tw-content: ''; 20 | } 21 | 22 | /* 23 | 1. Use a consistent sensible line-height in all browsers. 24 | 2. Prevent adjustments of font size after orientation changes in iOS. 25 | 3. Use a more readable tab size. 26 | 4. Use the user's configured `sans` font-family by default. 27 | */ 28 | 29 | html { 30 | line-height: 1.5; /* 1 */ 31 | -webkit-text-size-adjust: 100%; /* 2 */ 32 | -moz-tab-size: 4; /* 3 */ 33 | -o-tab-size: 4; 34 | tab-size: 4; /* 3 */ 35 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ 36 | } 37 | 38 | /* 39 | 1. Remove the margin in all browsers. 40 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 41 | */ 42 | 43 | body { 44 | margin: 0; /* 1 */ 45 | line-height: inherit; /* 2 */ 46 | } 47 | 48 | /* 49 | 1. Add the correct height in Firefox. 50 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 51 | 3. Ensure horizontal rules are visible by default. 52 | */ 53 | 54 | hr { 55 | height: 0; /* 1 */ 56 | color: inherit; /* 2 */ 57 | border-top-width: 1px; /* 3 */ 58 | } 59 | 60 | /* 61 | Add the correct text decoration in Chrome, Edge, and Safari. 62 | */ 63 | 64 | abbr:where([title]) { 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | } 68 | 69 | /* 70 | Remove the default font size and weight for headings. 71 | */ 72 | 73 | h1, 74 | h2, 75 | h3, 76 | h4, 77 | h5, 78 | h6 { 79 | font-size: inherit; 80 | font-weight: inherit; 81 | } 82 | 83 | /* 84 | Reset links to optimize for opt-in styling instead of opt-out. 85 | */ 86 | 87 | a { 88 | color: inherit; 89 | text-decoration: inherit; 90 | } 91 | 92 | /* 93 | Add the correct font weight in Edge and Safari. 94 | */ 95 | 96 | b, 97 | strong { 98 | font-weight: bolder; 99 | } 100 | 101 | /* 102 | 1. Use the user's configured `mono` font family by default. 103 | 2. Correct the odd `em` font sizing in all browsers. 104 | */ 105 | 106 | code, 107 | kbd, 108 | samp, 109 | pre { 110 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 111 | font-size: 1em; /* 2 */ 112 | } 113 | 114 | /* 115 | Add the correct font size in all browsers. 116 | */ 117 | 118 | small { 119 | font-size: 80%; 120 | } 121 | 122 | /* 123 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 124 | */ 125 | 126 | sub, 127 | sup { 128 | font-size: 75%; 129 | line-height: 0; 130 | position: relative; 131 | vertical-align: baseline; 132 | } 133 | 134 | sub { 135 | bottom: -0.25em; 136 | } 137 | 138 | sup { 139 | top: -0.5em; 140 | } 141 | 142 | /* 143 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 144 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 145 | 3. Remove gaps between table borders by default. 146 | */ 147 | 148 | table { 149 | text-indent: 0; /* 1 */ 150 | border-color: inherit; /* 2 */ 151 | border-collapse: collapse; /* 3 */ 152 | } 153 | 154 | /* 155 | 1. Change the font styles in all browsers. 156 | 2. Remove the margin in Firefox and Safari. 157 | 3. Remove default padding in all browsers. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | font-weight: inherit; /* 1 */ 168 | line-height: inherit; /* 1 */ 169 | color: inherit; /* 1 */ 170 | margin: 0; /* 2 */ 171 | padding: 0; /* 3 */ 172 | } 173 | 174 | /* 175 | Remove the inheritance of text transform in Edge and Firefox. 176 | */ 177 | 178 | button, 179 | select { 180 | text-transform: none; 181 | } 182 | 183 | /* 184 | 1. Correct the inability to style clickable types in iOS and Safari. 185 | 2. Remove default button styles. 186 | */ 187 | 188 | button, 189 | [type='button'], 190 | [type='reset'], 191 | [type='submit'] { 192 | -webkit-appearance: button; /* 1 */ 193 | background-color: transparent; /* 2 */ 194 | background-image: none; /* 2 */ 195 | } 196 | 197 | /* 198 | Use the modern Firefox focus style for all focusable elements. 199 | */ 200 | 201 | :-moz-focusring { 202 | outline: auto; 203 | } 204 | 205 | /* 206 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 207 | */ 208 | 209 | :-moz-ui-invalid { 210 | box-shadow: none; 211 | } 212 | 213 | /* 214 | Add the correct vertical alignment in Chrome and Firefox. 215 | */ 216 | 217 | progress { 218 | vertical-align: baseline; 219 | } 220 | 221 | /* 222 | Correct the cursor style of increment and decrement buttons in Safari. 223 | */ 224 | 225 | ::-webkit-inner-spin-button, 226 | ::-webkit-outer-spin-button { 227 | height: auto; 228 | } 229 | 230 | /* 231 | 1. Correct the odd appearance in Chrome and Safari. 232 | 2. Correct the outline style in Safari. 233 | */ 234 | 235 | [type='search'] { 236 | -webkit-appearance: textfield; /* 1 */ 237 | outline-offset: -2px; /* 2 */ 238 | } 239 | 240 | /* 241 | Remove the inner padding in Chrome and Safari on macOS. 242 | */ 243 | 244 | ::-webkit-search-decoration { 245 | -webkit-appearance: none; 246 | } 247 | 248 | /* 249 | 1. Correct the inability to style clickable types in iOS and Safari. 250 | 2. Change font properties to `inherit` in Safari. 251 | */ 252 | 253 | ::-webkit-file-upload-button { 254 | -webkit-appearance: button; /* 1 */ 255 | font: inherit; /* 2 */ 256 | } 257 | 258 | /* 259 | Add the correct display in Chrome and Safari. 260 | */ 261 | 262 | summary { 263 | display: list-item; 264 | } 265 | 266 | /* 267 | Removes the default spacing and border for appropriate elements. 268 | */ 269 | 270 | blockquote, 271 | dl, 272 | dd, 273 | h1, 274 | h2, 275 | h3, 276 | h4, 277 | h5, 278 | h6, 279 | hr, 280 | figure, 281 | p, 282 | pre { 283 | margin: 0; 284 | } 285 | 286 | fieldset { 287 | margin: 0; 288 | padding: 0; 289 | } 290 | 291 | legend { 292 | padding: 0; 293 | } 294 | 295 | ol, 296 | ul, 297 | menu { 298 | list-style: none; 299 | margin: 0; 300 | padding: 0; 301 | } 302 | 303 | /* 304 | Prevent resizing textareas horizontally by default. 305 | */ 306 | 307 | textarea { 308 | resize: vertical; 309 | } 310 | 311 | /* 312 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 313 | 2. Set the default placeholder color to the user's configured gray 400 color. 314 | */ 315 | 316 | input::-moz-placeholder, textarea::-moz-placeholder { 317 | opacity: 1; /* 1 */ 318 | color: #9ca3af; /* 2 */ 319 | } 320 | 321 | input::placeholder, 322 | textarea::placeholder { 323 | opacity: 1; /* 1 */ 324 | color: #9ca3af; /* 2 */ 325 | } 326 | 327 | /* 328 | Set the default cursor for buttons. 329 | */ 330 | 331 | button, 332 | [role="button"] { 333 | cursor: pointer; 334 | } 335 | 336 | /* 337 | Make sure disabled buttons don't get the pointer cursor. 338 | */ 339 | :disabled { 340 | cursor: default; 341 | } 342 | 343 | /* 344 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 345 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 346 | This can trigger a poorly considered lint error in some tools but is included by design. 347 | */ 348 | 349 | img, 350 | svg, 351 | video, 352 | canvas, 353 | audio, 354 | iframe, 355 | embed, 356 | object { 357 | display: block; /* 1 */ 358 | vertical-align: middle; /* 2 */ 359 | } 360 | 361 | /* 362 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 363 | */ 364 | 365 | img, 366 | video { 367 | max-width: 100%; 368 | height: auto; 369 | } 370 | 371 | *, ::before, ::after { 372 | --tw-border-spacing-x: 0; 373 | --tw-border-spacing-y: 0; 374 | --tw-translate-x: 0; 375 | --tw-translate-y: 0; 376 | --tw-rotate: 0; 377 | --tw-skew-x: 0; 378 | --tw-skew-y: 0; 379 | --tw-scale-x: 1; 380 | --tw-scale-y: 1; 381 | --tw-pan-x: ; 382 | --tw-pan-y: ; 383 | --tw-pinch-zoom: ; 384 | --tw-scroll-snap-strictness: proximity; 385 | --tw-ordinal: ; 386 | --tw-slashed-zero: ; 387 | --tw-numeric-figure: ; 388 | --tw-numeric-spacing: ; 389 | --tw-numeric-fraction: ; 390 | --tw-ring-inset: ; 391 | --tw-ring-offset-width: 0px; 392 | --tw-ring-offset-color: #fff; 393 | --tw-ring-color: rgb(59 130 246 / 0.5); 394 | --tw-ring-offset-shadow: 0 0 #0000; 395 | --tw-ring-shadow: 0 0 #0000; 396 | --tw-shadow: 0 0 #0000; 397 | --tw-shadow-colored: 0 0 #0000; 398 | --tw-blur: ; 399 | --tw-brightness: ; 400 | --tw-contrast: ; 401 | --tw-grayscale: ; 402 | --tw-hue-rotate: ; 403 | --tw-invert: ; 404 | --tw-saturate: ; 405 | --tw-sepia: ; 406 | --tw-drop-shadow: ; 407 | --tw-backdrop-blur: ; 408 | --tw-backdrop-brightness: ; 409 | --tw-backdrop-contrast: ; 410 | --tw-backdrop-grayscale: ; 411 | --tw-backdrop-hue-rotate: ; 412 | --tw-backdrop-invert: ; 413 | --tw-backdrop-opacity: ; 414 | --tw-backdrop-saturate: ; 415 | --tw-backdrop-sepia: ; 416 | } 417 | 418 | ::-webkit-backdrop { 419 | --tw-border-spacing-x: 0; 420 | --tw-border-spacing-y: 0; 421 | --tw-translate-x: 0; 422 | --tw-translate-y: 0; 423 | --tw-rotate: 0; 424 | --tw-skew-x: 0; 425 | --tw-skew-y: 0; 426 | --tw-scale-x: 1; 427 | --tw-scale-y: 1; 428 | --tw-pan-x: ; 429 | --tw-pan-y: ; 430 | --tw-pinch-zoom: ; 431 | --tw-scroll-snap-strictness: proximity; 432 | --tw-ordinal: ; 433 | --tw-slashed-zero: ; 434 | --tw-numeric-figure: ; 435 | --tw-numeric-spacing: ; 436 | --tw-numeric-fraction: ; 437 | --tw-ring-inset: ; 438 | --tw-ring-offset-width: 0px; 439 | --tw-ring-offset-color: #fff; 440 | --tw-ring-color: rgb(59 130 246 / 0.5); 441 | --tw-ring-offset-shadow: 0 0 #0000; 442 | --tw-ring-shadow: 0 0 #0000; 443 | --tw-shadow: 0 0 #0000; 444 | --tw-shadow-colored: 0 0 #0000; 445 | --tw-blur: ; 446 | --tw-brightness: ; 447 | --tw-contrast: ; 448 | --tw-grayscale: ; 449 | --tw-hue-rotate: ; 450 | --tw-invert: ; 451 | --tw-saturate: ; 452 | --tw-sepia: ; 453 | --tw-drop-shadow: ; 454 | --tw-backdrop-blur: ; 455 | --tw-backdrop-brightness: ; 456 | --tw-backdrop-contrast: ; 457 | --tw-backdrop-grayscale: ; 458 | --tw-backdrop-hue-rotate: ; 459 | --tw-backdrop-invert: ; 460 | --tw-backdrop-opacity: ; 461 | --tw-backdrop-saturate: ; 462 | --tw-backdrop-sepia: ; 463 | } 464 | 465 | ::backdrop { 466 | --tw-border-spacing-x: 0; 467 | --tw-border-spacing-y: 0; 468 | --tw-translate-x: 0; 469 | --tw-translate-y: 0; 470 | --tw-rotate: 0; 471 | --tw-skew-x: 0; 472 | --tw-skew-y: 0; 473 | --tw-scale-x: 1; 474 | --tw-scale-y: 1; 475 | --tw-pan-x: ; 476 | --tw-pan-y: ; 477 | --tw-pinch-zoom: ; 478 | --tw-scroll-snap-strictness: proximity; 479 | --tw-ordinal: ; 480 | --tw-slashed-zero: ; 481 | --tw-numeric-figure: ; 482 | --tw-numeric-spacing: ; 483 | --tw-numeric-fraction: ; 484 | --tw-ring-inset: ; 485 | --tw-ring-offset-width: 0px; 486 | --tw-ring-offset-color: #fff; 487 | --tw-ring-color: rgb(59 130 246 / 0.5); 488 | --tw-ring-offset-shadow: 0 0 #0000; 489 | --tw-ring-shadow: 0 0 #0000; 490 | --tw-shadow: 0 0 #0000; 491 | --tw-shadow-colored: 0 0 #0000; 492 | --tw-blur: ; 493 | --tw-brightness: ; 494 | --tw-contrast: ; 495 | --tw-grayscale: ; 496 | --tw-hue-rotate: ; 497 | --tw-invert: ; 498 | --tw-saturate: ; 499 | --tw-sepia: ; 500 | --tw-drop-shadow: ; 501 | --tw-backdrop-blur: ; 502 | --tw-backdrop-brightness: ; 503 | --tw-backdrop-contrast: ; 504 | --tw-backdrop-grayscale: ; 505 | --tw-backdrop-hue-rotate: ; 506 | --tw-backdrop-invert: ; 507 | --tw-backdrop-opacity: ; 508 | --tw-backdrop-saturate: ; 509 | --tw-backdrop-sepia: ; 510 | } 511 | .p-8 { 512 | padding: 2rem; 513 | } 514 | .font-sans { 515 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 516 | } 517 | .text-3xl { 518 | font-size: 1.875rem; 519 | line-height: 2.25rem; 520 | } 521 | .font-bold { 522 | font-weight: 700; 523 | } 524 | .text-gray-900 { 525 | --tw-text-opacity: 1; 526 | color: rgb(17 24 39 / var(--tw-text-opacity)); 527 | } 528 | .antialiased { 529 | -webkit-font-smoothing: antialiased; 530 | -moz-osx-font-smoothing: grayscale; 531 | } 532 | 533 | -------------------------------------------------------------------------------- /stubs/site/source/assets/build/js/main.js: -------------------------------------------------------------------------------- 1 | /******/ (() => { // webpackBootstrap 2 | /******/ var __webpack_modules__ = ({ 3 | 4 | /***/ "./source/_assets/js/main.js": 5 | /*!***********************************!*\ 6 | !*** ./source/_assets/js/main.js ***! 7 | \***********************************/ 8 | /***/ (() => { 9 | 10 | 11 | 12 | /***/ }), 13 | 14 | /***/ "./source/_assets/css/main.css": 15 | /*!*************************************!*\ 16 | !*** ./source/_assets/css/main.css ***! 17 | \*************************************/ 18 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 19 | 20 | "use strict"; 21 | __webpack_require__.r(__webpack_exports__); 22 | // extracted by mini-css-extract-plugin 23 | 24 | 25 | /***/ }) 26 | 27 | /******/ }); 28 | /************************************************************************/ 29 | /******/ // The module cache 30 | /******/ var __webpack_module_cache__ = {}; 31 | /******/ 32 | /******/ // The require function 33 | /******/ function __webpack_require__(moduleId) { 34 | /******/ // Check if module is in cache 35 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 36 | /******/ if (cachedModule !== undefined) { 37 | /******/ return cachedModule.exports; 38 | /******/ } 39 | /******/ // Create a new module (and put it into the cache) 40 | /******/ var module = __webpack_module_cache__[moduleId] = { 41 | /******/ // no module.id needed 42 | /******/ // no module.loaded needed 43 | /******/ exports: {} 44 | /******/ }; 45 | /******/ 46 | /******/ // Execute the module function 47 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 48 | /******/ 49 | /******/ // Return the exports of the module 50 | /******/ return module.exports; 51 | /******/ } 52 | /******/ 53 | /******/ // expose the modules object (__webpack_modules__) 54 | /******/ __webpack_require__.m = __webpack_modules__; 55 | /******/ 56 | /************************************************************************/ 57 | /******/ /* webpack/runtime/chunk loaded */ 58 | /******/ (() => { 59 | /******/ var deferred = []; 60 | /******/ __webpack_require__.O = (result, chunkIds, fn, priority) => { 61 | /******/ if(chunkIds) { 62 | /******/ priority = priority || 0; 63 | /******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1]; 64 | /******/ deferred[i] = [chunkIds, fn, priority]; 65 | /******/ return; 66 | /******/ } 67 | /******/ var notFulfilled = Infinity; 68 | /******/ for (var i = 0; i < deferred.length; i++) { 69 | /******/ var [chunkIds, fn, priority] = deferred[i]; 70 | /******/ var fulfilled = true; 71 | /******/ for (var j = 0; j < chunkIds.length; j++) { 72 | /******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) { 73 | /******/ chunkIds.splice(j--, 1); 74 | /******/ } else { 75 | /******/ fulfilled = false; 76 | /******/ if(priority < notFulfilled) notFulfilled = priority; 77 | /******/ } 78 | /******/ } 79 | /******/ if(fulfilled) { 80 | /******/ deferred.splice(i--, 1) 81 | /******/ var r = fn(); 82 | /******/ if (r !== undefined) result = r; 83 | /******/ } 84 | /******/ } 85 | /******/ return result; 86 | /******/ }; 87 | /******/ })(); 88 | /******/ 89 | /******/ /* webpack/runtime/hasOwnProperty shorthand */ 90 | /******/ (() => { 91 | /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) 92 | /******/ })(); 93 | /******/ 94 | /******/ /* webpack/runtime/make namespace object */ 95 | /******/ (() => { 96 | /******/ // define __esModule on exports 97 | /******/ __webpack_require__.r = (exports) => { 98 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 99 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 100 | /******/ } 101 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 102 | /******/ }; 103 | /******/ })(); 104 | /******/ 105 | /******/ /* webpack/runtime/jsonp chunk loading */ 106 | /******/ (() => { 107 | /******/ // no baseURI 108 | /******/ 109 | /******/ // object to store loaded and loading chunks 110 | /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched 111 | /******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded 112 | /******/ var installedChunks = { 113 | /******/ "/js/main": 0, 114 | /******/ "css/main": 0 115 | /******/ }; 116 | /******/ 117 | /******/ // no chunk on demand loading 118 | /******/ 119 | /******/ // no prefetching 120 | /******/ 121 | /******/ // no preloaded 122 | /******/ 123 | /******/ // no HMR 124 | /******/ 125 | /******/ // no HMR manifest 126 | /******/ 127 | /******/ __webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0); 128 | /******/ 129 | /******/ // install a JSONP callback for chunk loading 130 | /******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => { 131 | /******/ var [chunkIds, moreModules, runtime] = data; 132 | /******/ // add "moreModules" to the modules object, 133 | /******/ // then flag all "chunkIds" as loaded and fire callback 134 | /******/ var moduleId, chunkId, i = 0; 135 | /******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) { 136 | /******/ for(moduleId in moreModules) { 137 | /******/ if(__webpack_require__.o(moreModules, moduleId)) { 138 | /******/ __webpack_require__.m[moduleId] = moreModules[moduleId]; 139 | /******/ } 140 | /******/ } 141 | /******/ if(runtime) var result = runtime(__webpack_require__); 142 | /******/ } 143 | /******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data); 144 | /******/ for(;i < chunkIds.length; i++) { 145 | /******/ chunkId = chunkIds[i]; 146 | /******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { 147 | /******/ installedChunks[chunkId][0](); 148 | /******/ } 149 | /******/ installedChunks[chunkId] = 0; 150 | /******/ } 151 | /******/ return __webpack_require__.O(result); 152 | /******/ } 153 | /******/ 154 | /******/ var chunkLoadingGlobal = self["webpackChunk"] = self["webpackChunk"] || []; 155 | /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0)); 156 | /******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); 157 | /******/ })(); 158 | /******/ 159 | /************************************************************************/ 160 | /******/ 161 | /******/ // startup 162 | /******/ // Load entry module and return exports 163 | /******/ // This entry module depends on other loaded chunks and execution need to be delayed 164 | /******/ __webpack_require__.O(undefined, ["css/main"], () => (__webpack_require__("./source/_assets/js/main.js"))) 165 | /******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["css/main"], () => (__webpack_require__("./source/_assets/css/main.css"))) 166 | /******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__); 167 | /******/ 168 | /******/ })() 169 | ; -------------------------------------------------------------------------------- /stubs/site/source/assets/build/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/main.js": "/js/main.js?id=2f836786f79f4c701101ba20385775d4", 3 | "/css/main.css": "/css/main.css?id=c5ea2eafbdeaf467f0035901e0dd5efb" 4 | } 5 | -------------------------------------------------------------------------------- /stubs/site/source/assets/images/jigsaw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tighten/jigsaw/4ca1777989031ee628ad03fee3e44686e130a0ee/stubs/site/source/assets/images/jigsaw.png -------------------------------------------------------------------------------- /stubs/site/source/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('_layouts.main') 2 | 3 | @section('body') 4 |
5 |

Hello world!

6 |
7 | @endsection 8 | -------------------------------------------------------------------------------- /stubs/site/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: require('fast-glob').sync([ 3 | 'source/**/*.{blade.php,blade.md,md,html,vue}', 4 | '!source/**/_tmp/*' // exclude temporary files 5 | ],{ dot: true }), 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | }; 11 | -------------------------------------------------------------------------------- /stubs/site/webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | require('laravel-mix-jigsaw'); 3 | 4 | mix.disableSuccessNotifications(); 5 | mix.setPublicPath('source/assets/build'); 6 | 7 | mix.jigsaw() 8 | .js('source/_assets/js/main.js', 'js') 9 | .css('source/_assets/css/main.css', 'css', [ 10 | require('postcss-import'), 11 | require('tailwindcss'), 12 | ]) 13 | .options({ 14 | processCssUrls: false, 15 | }) 16 | .version(); 17 | --------------------------------------------------------------------------------