├── .editorconfig ├── .gitattributes ├── .gitignore ├── composer.json ├── composer.lock ├── license.md ├── readme.md └── src ├── Cache ├── Component.php ├── Drivers │ ├── Driver.php │ ├── File.php │ └── JsonFile.php └── Registry.php ├── Content ├── Entry │ ├── Entry.php │ ├── File.php │ ├── MarkdownFile.php │ └── Virtual.php ├── Locator │ └── File.php ├── Query │ └── File.php └── Type │ ├── Component.php │ ├── Type.php │ └── Types.php ├── Contracts ├── Bootable.php ├── Cache │ ├── Driver.php │ └── Registry.php ├── CastsToHtml.php ├── CastsToText.php ├── Content │ ├── ContentEntry.php │ ├── ContentLocator.php │ ├── ContentQuery.php │ ├── ContentType.php │ └── ContentTypes.php ├── Core │ ├── Application.php │ └── Container.php ├── Displayable.php ├── Feed │ └── FeedWriter.php ├── Makeable.php ├── Markdown │ └── Parser.php ├── Renderable.php ├── Routing │ ├── RoutingRoute.php │ ├── RoutingRouter.php │ ├── RoutingRoutes.php │ └── RoutingUrl.php └── Template │ ├── TemplateEngine.php │ ├── TemplateTag.php │ ├── TemplateTags.php │ └── TemplateView.php ├── Controllers ├── Cache.php ├── Collection.php ├── CollectionArchiveDate.php ├── CollectionFeed.php ├── CollectionFeedAtom.php ├── CollectionTaxonomyTerm.php ├── Controller.php ├── Error404.php ├── Home.php ├── Single.php ├── SinglePage.php ├── Sitemap.php └── SitemapIndex.php ├── Core ├── Application.php ├── Container.php ├── Providers │ ├── App.php │ ├── Cache.php │ ├── Content.php │ ├── Markdown.php │ ├── Routing.php │ └── Template.php ├── Proxies │ ├── App.php │ ├── Cache.php │ ├── Config.php │ ├── Engine.php │ ├── Message.php │ ├── PoweredBy.php │ ├── Query.php │ └── Url.php ├── Proxy.php ├── Schemas │ ├── App.php │ ├── Cache.php │ ├── Content.php │ ├── Markdown.php │ └── Template.php └── ServiceProvider.php ├── Feed └── Writer.php ├── Markdown ├── ImageRenderer.php ├── LinkRenderer.php ├── ParagraphRenderer.php └── Parser.php ├── Messenger └── Message.php ├── Routing ├── Component.php ├── Route │ ├── Route.php │ └── Routes.php ├── Router.php └── Url.php ├── Template ├── Component.php ├── Engine.php ├── Hierarchy.php ├── Tag │ ├── DocumentTitle.php │ ├── Pagination.php │ ├── PoweredBy.php │ ├── Tag.php │ └── Tags.php └── View.php ├── Tools ├── Collection.php ├── Media.php └── Str.php ├── functions-deprecated.php └── functions-helpers.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = tab 8 | 9 | # 2 space indentation 10 | [*.yaml] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Handle line endings. 2 | # See https://help.github.com/articles/dealing-with-line-endings/ 3 | 4 | * text auto 5 | *.gif binary 6 | *.ico binary 7 | *.jpg binary 8 | *.png binary 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /vendor 3 | /phpcs.xml 4 | /.phpcs.xml 5 | *.map 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "blush-dev/framework", 3 | "description" : "Blush Framework: Foundation of the Blush CMS, a flat-file CMS.", 4 | "keywords" : [ "cms" ], 5 | "homepage" : "https://github.com/blush-dev/framework", 6 | "license" : "GPL-2.0-or-later", 7 | "authors" : [ 8 | { 9 | "name" : "Justin Tadlock", 10 | "email" : "justintadlock@gmail.com", 11 | "homepage" : "http://justintadlock.com", 12 | "role" : "Developer" 13 | } 14 | ], 15 | "support" : { 16 | "issues" : "https://github.com/blush-dev/framework/issues" 17 | }, 18 | "autoload" : { 19 | "psr-4" : { 20 | "Blush\\" : "src/" 21 | }, 22 | "files" : [ 23 | "src/functions-helpers.php", 24 | "src/functions-deprecated.php" 25 | ] 26 | }, 27 | "require": { 28 | "php": ">=8.0", 29 | "symfony/http-foundation": "5.4.x-dev", 30 | "symfony/yaml": "^3.0", 31 | "league/commonmark": "^2.2", 32 | "symfony/var-dumper": "^5.4", 33 | "vlucas/phpdotenv": "^5.4", 34 | "league/config": "^1.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 - 2022, Justin Tadlock 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Blush CMS/Framework 2 | 3 | Blush is a flat-file system (_can we really call this hot mess a CMS?_) for creating super basic websites. This is the source for the framework. It is under super-duper-crazy-heavy development at the moment. So, you know, don't run this in production. 4 | 5 | To test it, install the `blush-dev/blush` repo. 6 | -------------------------------------------------------------------------------- /src/Cache/Component.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 11 | * @link https://github.com/blush-dev/framework 12 | * @license https://opensource.org/licenses/MIT 13 | */ 14 | 15 | namespace Blush\Cache; 16 | 17 | // Contracts. 18 | use Blush\Contracts\Bootable; 19 | use Blush\Contracts\Cache\Registry; 20 | 21 | // Classes. 22 | use Blush\Config; 23 | 24 | class Component implements Bootable 25 | { 26 | /** 27 | * Sets up object state. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function __construct( 32 | protected Registry $registry, 33 | protected array $drivers, 34 | protected array $stores 35 | ) {} 36 | 37 | /** 38 | * Bootstraps the component, setting up cache drivers and stores. 39 | * 40 | * @since 1.0.0 41 | */ 42 | public function boot(): void 43 | { 44 | // Add drivers to the cache registry. 45 | foreach ( $this->drivers as $name => $driver ) { 46 | $this->registry->addDriver( $name, $driver ); 47 | } 48 | 49 | // Add stores to the cache registry. 50 | foreach ( $this->stores as $name => $options ) { 51 | $this->registry->addStore( $name, $options ); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Cache/Drivers/File.php: -------------------------------------------------------------------------------- 1 | 13 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 14 | * @link https://github.com/blush-dev/framework 15 | * @license https://opensource.org/licenses/MIT 16 | */ 17 | 18 | namespace Blush\Cache\Drivers; 19 | 20 | use Closure; 21 | use Blush\Tools\Str; 22 | 23 | class File extends Driver 24 | { 25 | /** 26 | * The store's full directory path. 27 | * 28 | * @since 1.0.0 29 | */ 30 | protected string $path; 31 | 32 | /** 33 | * File extenstion for the store's files without the preceding dot. 34 | * 35 | * @since 1.0.0 36 | */ 37 | protected string $extension = 'cache'; 38 | 39 | /** 40 | * Sets up object state. 41 | * 42 | * @since 1.0.0 43 | */ 44 | public function __construct( protected string $store, array $options = [] ) 45 | { 46 | $this->path = $options['path'] ?? cache_path( $this->store ); 47 | } 48 | 49 | /** 50 | * Creates the store directory. 51 | * 52 | * @since 1.0.0 53 | */ 54 | public function make(): self 55 | { 56 | if ( ! file_exists( $this->path() ) ) { 57 | mkdir( $this->path(), 0775, true ); 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Returns the store's full directory path. 65 | * 66 | * @since 1.0.0 67 | */ 68 | protected function path(): string 69 | { 70 | return $this->path; 71 | } 72 | 73 | /** 74 | * Returns a store cache's filepath. 75 | * 76 | * @since 1.0.0 77 | */ 78 | protected function filepath( string $key ): string 79 | { 80 | $ext = trim( $this->extension, '.' ); 81 | return Str::appendPath( $this->path(), "{$key}.{$ext}" ); 82 | } 83 | 84 | /** 85 | * Checks if a store cache's filepath exists. 86 | * 87 | * @since 1.0.0 88 | */ 89 | protected function fileExists( string $key ): bool 90 | { 91 | return file_exists( $this->filepath( $key ) ); 92 | } 93 | 94 | /** 95 | * Check if the store has data by cache key. 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function has( string $key ): bool 100 | { 101 | return $this->hasData( $key ) || $this->fileExists( $key ); 102 | } 103 | 104 | /** 105 | * Returns data from a store by cache key. 106 | * 107 | * @since 1.0.0 108 | */ 109 | public function get( string $key ): mixed 110 | { 111 | if ( $this->hasData( $key ) ) { 112 | return $this->getData( $key ); 113 | } 114 | 115 | if ( $this->fileExists( $key ) ) { 116 | $data = file_get_contents( $this->filepath( $key ) ); 117 | $data = unserialize( $data ); 118 | 119 | if ( $this->hasExpired( $data ) ) { 120 | $this->forget( $key ); 121 | return null; 122 | } 123 | 124 | $this->setData( $key, $data ); 125 | } 126 | 127 | return $this->data[$key]['data'] ?? null; 128 | } 129 | 130 | /** 131 | * Writes new data or replaces existing data by cache key. 132 | * 133 | * @since 1.0.0 134 | */ 135 | public function put( string $key, mixed $data, int $seconds = 0 ): bool 136 | { 137 | $data = serialize( [ 138 | 'meta' => [ 139 | 'expires' => $this->availableAt( $seconds ), 140 | 'created' => $this->createdAt() 141 | ], 142 | 'data' => $data 143 | ] ); 144 | 145 | $put = file_put_contents( $this->filepath( $key ), $data ); 146 | 147 | if ( false !== $put ) { 148 | $this->setData( $key, $data ); 149 | } 150 | 151 | // `file_put_contents()` returns `int|false`. 152 | return false !== $put; 153 | } 154 | 155 | /** 156 | * Writes new data if it doesn't exist by cache key. 157 | * 158 | * @since 1.0.0 159 | */ 160 | public function add( string $key, $data, int $seconds = 0 ): bool 161 | { 162 | if ( ! $this->fileExists( $key ) ) { 163 | return $this->put( $key, $data, $seconds ); 164 | } 165 | 166 | return false; 167 | } 168 | 169 | /** 170 | * Deletes data if it exists by cache key. 171 | * 172 | * @since 1.0.0 173 | */ 174 | public function forget( string $key ): bool 175 | { 176 | if ( $this->fileExists( $key ) ) { 177 | $this->removeData( $key ); 178 | return unlink( $this->filepath( $key ) ); 179 | } 180 | 181 | return false; 182 | } 183 | 184 | /** 185 | * Writes new data if it doesn't exist by cache key. Doesn't expire. 186 | * 187 | * @since 1.0.0 188 | */ 189 | public function forever( string $key, $data ): bool 190 | { 191 | return $this->add( $key, $data, 0 ); 192 | } 193 | 194 | /** 195 | * Gets and returns data by cache key. If it doesn't exist, callback is 196 | * executed to pass in custom data and write it. 197 | * 198 | * @since 1.0.0 199 | */ 200 | public function remember( string $key, int $seconds, Closure $callback ): mixed 201 | { 202 | $data = $this->get( $key ); 203 | 204 | if ( ! $data ) { 205 | $data = $callback(); 206 | if ( $data ) { 207 | $this->put( $key, $data, $seconds ); 208 | } 209 | } 210 | 211 | return $data; 212 | } 213 | 214 | /** 215 | * Gets and returns data by cache key. If it doesn't exist, callback is 216 | * executed to pass in custom data and write it. Doesn't expire. 217 | * 218 | * @since 1.0.0 219 | */ 220 | public function rememberForever( string $key, Closure $callback ): mixed 221 | { 222 | return $this->remember( $key, 0, $callback ); 223 | } 224 | 225 | /** 226 | * Gets and returns data by key. Deletes previous data. 227 | * 228 | * @since 1.0.0 229 | */ 230 | public function pull( string $key ): mixed 231 | { 232 | $data = $this->get( $key ); 233 | 234 | $this->forget( $key ); 235 | 236 | return $data; 237 | } 238 | 239 | /** 240 | * Deletes all cached data from a store. 241 | * 242 | * @since 1.0.0 243 | */ 244 | public function flush(): void 245 | { 246 | $ext = trim( $this->extension, '.' ); 247 | $search = Str::appendPath( $this->path(), "*.{$ext}" ); 248 | 249 | foreach ( glob( $search ) as $filepath ) { 250 | unlink( $filepath ); 251 | } 252 | 253 | $this->resetData(); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/Cache/Drivers/JsonFile.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Cache\Drivers; 13 | 14 | use Blush\Tools\Str; 15 | 16 | class JsonFile extends File 17 | { 18 | /** 19 | * File extenstion for the store's files without the preceding dot. 20 | * 21 | * @since 1.0.0 22 | */ 23 | protected string $extension = 'json'; 24 | 25 | /** 26 | * Returns data from a store by cache key. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function get( string $key ): mixed 31 | { 32 | if ( $this->hasData( $key ) ) { 33 | return $this->getData( $key ); 34 | } 35 | 36 | if ( $data = $this->getJsonFileContents( $key ) ) { 37 | 38 | if ( $this->hasExpired( $data ) ) { 39 | $this->forget( $key ); 40 | return null; 41 | } 42 | 43 | $this->setData( $key, $data ); 44 | } 45 | 46 | return $this->data[$key]['data'] ?? null; 47 | } 48 | 49 | /** 50 | * Writes new data or replaces existing data by cache key. 51 | * 52 | * @since 1.0.0 53 | */ 54 | public function put( string $key, mixed $data, int $seconds = 0 ): bool 55 | { 56 | $put = $this->putJsonFileContents( $key, $data, $seconds ); 57 | 58 | if ( true === $put ) { 59 | $this->setData( $key, $data ); 60 | } 61 | 62 | return $put; 63 | } 64 | 65 | /** 66 | * Gets the cache file contents by key and runs it through `json_decode()`. 67 | * 68 | * @since 1.0.0 69 | */ 70 | protected function getJsonFileContents( string $key ): array|false 71 | { 72 | if ( ! $this->fileExists( $key ) ) { 73 | return false; 74 | } 75 | 76 | $contents = file_get_contents( $this->filepath( $key ) ); 77 | 78 | $decoded = $contents ? json_decode( $contents, true ) : false; 79 | 80 | return $decoded ?: false; 81 | } 82 | 83 | /** 84 | * Encodes an array of data to JSON and writes it to the file path. 85 | * 86 | * @since 1.0.0 87 | */ 88 | protected function putJsonFileContents( string $key, array $data, int $seconds ): bool 89 | { 90 | $data = json_encode( [ 91 | 'meta' => [ 92 | 'expires' => $this->availableAt( $seconds ), 93 | 'created' => $this->createdAt() 94 | ], 95 | 'data' => $data 96 | ], JSON_PRETTY_PRINT ); 97 | 98 | $put = file_put_contents( $this->filepath( $key ), $data ); 99 | 100 | // `file_put_contents()` returns `int|false`. 101 | return false !== $put; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Content/Entry/File.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Content\Entry; 13 | 14 | // Abstracts. 15 | use Blush\Contracts\Content\ContentType; 16 | 17 | // Concretes. 18 | use Blush\Core\Proxies\{App, Url}; 19 | use Blush\Tools\Str; 20 | 21 | abstract class File extends Entry 22 | { 23 | /** 24 | * Entry path info. 25 | * 26 | * @since 1.0.0 27 | */ 28 | protected array $pathinfo = []; 29 | 30 | /** 31 | * Sets up the object state. Child classes need to overwrite this and 32 | * pull content and metadata from the file path. 33 | * 34 | * @since 1.0.0 35 | */ 36 | public function __construct( protected string $filepath ) 37 | { 38 | $this->pathinfo = pathinfo( $filepath ); 39 | } 40 | 41 | /** 42 | * Returns the entry file path. 43 | * 44 | * @since 1.0.0 45 | */ 46 | protected function filepath(): string 47 | { 48 | return $this->filepath; 49 | } 50 | 51 | /** 52 | * Returns the file's pathinfo or a specific value. 53 | * 54 | * @since 1.0.0 55 | */ 56 | protected function pathinfo( string $key = '' ): array|string 57 | { 58 | if ( $key ) { 59 | return $this->pathinfo[ $key ] ?? ''; 60 | } 61 | 62 | return $this->pathinfo; 63 | } 64 | 65 | /** 66 | * Returns the file's directory name. 67 | * 68 | * @since 1.0.0 69 | */ 70 | protected function dirname(): string 71 | { 72 | return $this->pathinfo( 'dirname' ); 73 | } 74 | 75 | /** 76 | * Returns the file's basename (includes extension). 77 | * 78 | * @since 1.0.0 79 | */ 80 | protected function basename(): string 81 | { 82 | return $this->pathinfo( 'basename' ); 83 | } 84 | 85 | /** 86 | * Returns the file's extension. 87 | * 88 | * @since 1.0.0 89 | */ 90 | protected function extension(): string 91 | { 92 | return $this->pathinfo( 'extension' ); 93 | } 94 | 95 | /** 96 | * Returns the filename without extension. 97 | * 98 | * @since 1.0.0 99 | */ 100 | protected function filename(): string 101 | { 102 | return $this->pathinfo( 'filename' ); 103 | } 104 | 105 | /** 106 | * Returns the entry type. 107 | * 108 | * @since 1.0.0 109 | */ 110 | public function type(): ContentType 111 | { 112 | // Return type if it's already set. 113 | if ( $this->type ) { 114 | return $this->type; 115 | } 116 | 117 | $types = App::get( 'content.types' ); 118 | $has_type = false; 119 | 120 | // Strip the file basename and content path from the file path. 121 | // This should give us the content type path, which we can match 122 | // against registered content types. 123 | $path = Str::beforeLast( $this->filepath(), basename( $this->filepath() ) ); 124 | $path = Str::afterLast( $path, App::get( 'path.content' ) ); 125 | $path = Str::trimSlashes( $path ); 126 | 127 | // Get the content type by path. 128 | if ( $path ) { 129 | $has_type = $types->getTypeFromPath( $path ); 130 | } 131 | 132 | // Set type or fall back to the `page` type. 133 | $this->type = $has_type ?: $types->get( 'page' ); 134 | 135 | return $this->type; 136 | } 137 | 138 | /** 139 | * Returns the entry name (slug). 140 | * 141 | * @since 1.0.0 142 | */ 143 | public function name(): string 144 | { 145 | // Get the filename without the extension. 146 | $name = $this->filename(); 147 | 148 | // Strip anything before potential ordering dot, e.g., 149 | // `01.{$name}`, `02.{$name}`, etc. 150 | if ( Str::contains( $name, '.' ) ) { 151 | $name = Str::afterLast( $name, '.' ); 152 | } 153 | 154 | // If the name is 'index', let's base it on the directory. 155 | if ( 'index' === $name && Str::contains( $this->filepath(), '/' ) ) { 156 | $path = str_replace( content_path(), '', $this->filepath() ); 157 | $_name = Str::trimSlashes( Str::beforeLast( $path, '/index' ) ); 158 | $name = $_name ?: $name; 159 | } 160 | 161 | return $name; 162 | } 163 | 164 | /** 165 | * Returns a post's visibility. Currently, the API allows for 166 | * `public` and `hidden`. 167 | * 168 | * @since 1.0.0 169 | */ 170 | public function visibility(): string 171 | { 172 | return Str::startsWith( $this->filename(), '_' ) 173 | ? 'hidden' 174 | : parent::visibility(); 175 | } 176 | 177 | /** 178 | * Returns the entry URL. 179 | * 180 | * @since 1.0.0 181 | */ 182 | public function url(): string 183 | { 184 | // If dealing with a page, build the URL from the filepath. 185 | if ( 'page' === $this->type()->name() ) { 186 | 187 | // Strip the content path from the directory name. 188 | $url_path = Str::afterLast( $this->dirname(), content_path() ); 189 | 190 | // If this is not the `index` file, append the filename 191 | // to the URL path. 192 | if ( 'index' !== $this->filename() ) { 193 | $url_path = Str::appendPath( 194 | $url_path, 195 | $this->filename() 196 | ); 197 | } 198 | 199 | return Url::to( $url_path ); 200 | } 201 | 202 | // If this is the index file, it is the content type archive, 203 | // so we'll just return early with the type's URL. 204 | if ( 'index' === $this->filename() ) { 205 | return $this->type->url(); 206 | } 207 | 208 | // Adds the required name param. 209 | $params = [ 'name' => $this->name() ]; 210 | 211 | // Get `published` meta and add back-compat for `date`. 212 | $date = $this->metaSingle( 'published' ); 213 | $date = $date ?: $this->metaSingle( 'date' ); 214 | 215 | // Adds date-based params if we have a date. 216 | if ( $date ) { 217 | $timestamp = is_numeric( $date ) ? $date : strtotime( $date ); 218 | $date = date( 'Y-m-d', $timestamp ); 219 | $time = date( 'H:i:s', $timestamp ); 220 | 221 | $params['year'] = Str::beforeFirst( $date, '-' ); 222 | $params['month'] = Str::between( $date, '-', '-' ); 223 | $params['day'] = Str::afterLast( $date, '-' ); 224 | $params['hour'] = Str::beforeFirst( $time, ':' ); 225 | $params['minute'] = Str::between( $time, ':', ':' ); 226 | $params['second'] = Str::afterLast( $time, ':' ); 227 | } 228 | 229 | // Add author param if author exists. 230 | if ( $author = $this->metaSingle( 'author' ) ) { 231 | $params['author'] = sanitize_slug( $author ); 232 | } 233 | 234 | // Add content type/taxonomy params if they exist. 235 | foreach ( App::get( 'content.types' ) as $type ) { 236 | if ( $slug = $this->metaSingle( $type->name() ) ) { 237 | $params[ $type->name() ] = $slug; 238 | } 239 | } 240 | 241 | // Build the URL based on the route. 242 | return $this->type()->singleUrl( $params ); 243 | } 244 | 245 | /** 246 | * Returns the entry updated datetime. 247 | * 248 | * @since 1.0.0 249 | */ 250 | public function updated( string $format = '' ): string 251 | { 252 | // Returned updated meta if found. 253 | if ( $updated = parent::updated( 'updated', $format ) ) { 254 | return $updated; 255 | } 256 | 257 | // Fall back to file's modified time. 258 | return $this->date( filemtime( $this->filepath() ) ); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/Content/Entry/MarkdownFile.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Content\Entry; 13 | 14 | use Blush\Core\Proxies\{App, Cache, Config}; 15 | use Blush\Tools\Str; 16 | use Symfony\Component\Yaml\Yaml; 17 | 18 | class MarkdownFile extends File 19 | { 20 | /** 21 | * Whether Markdown has been parsed. 22 | * 23 | * @since 1.0.0 24 | */ 25 | protected bool $markdown_parsed = false; 26 | 27 | /** 28 | * Whether YAML has been parsed. 29 | * 30 | * @since 1.0.0 31 | */ 32 | protected bool $yaml_parsed = false; 33 | 34 | /** 35 | * Whether this is a "no content" request. 36 | * 37 | * @since 1.0.0 38 | */ 39 | protected bool $nocontent = false; 40 | 41 | /** 42 | * Sets up the object state. Child classes need to overwrite this and 43 | * pull content and metadata from the file path. 44 | * 45 | * @since 1.0.0 46 | */ 47 | public function __construct( protected string $filepath, array $options = [] ) 48 | { 49 | $this->nocontent = $options['nocontent'] ?? false; 50 | 51 | parent::__construct( $filepath ); 52 | } 53 | 54 | /** 55 | * Returns the entry content. 56 | * 57 | * @since 1.0.0 58 | */ 59 | public function content(): string 60 | { 61 | if ( ! $this->isMarkdownParsed() ) { 62 | $this->parseMarkdown(); 63 | } 64 | 65 | return parent::content(); 66 | } 67 | 68 | /** 69 | * Returns entry metadata. 70 | * 71 | * @since 1.0.0 72 | */ 73 | public function meta( string $name = '', mixed $default = false ): mixed 74 | { 75 | if ( $this->nocontent && ! $this->isYamlParsed() ) { 76 | $this->parseYaml(); 77 | } elseif ( ! $this->nocontent && ! $this->isMarkdownParsed() ) { 78 | $this->parseMarkdown(); 79 | } 80 | 81 | return parent::meta( $name, $default ); 82 | } 83 | 84 | /** 85 | * Conditional for determining whether the Markdown has been parsed. 86 | * 87 | * @since 1.0.0 88 | */ 89 | protected function isMarkdownParsed(): bool 90 | { 91 | return $this->markdown_parsed; 92 | } 93 | 94 | /** 95 | * Conditional for determining whether the Markdown has been parsed. 96 | * 97 | * @since 1.0.0 98 | */ 99 | protected function isYamlParsed(): bool 100 | { 101 | return $this->yaml_parsed; 102 | } 103 | 104 | /** 105 | * Just-in-time Markdown parsing. This should not be called unless 106 | * Markdown has yet to be parsed. 107 | * 108 | * @since 1.0.0 109 | */ 110 | protected function parseMarkdown(): void 111 | { 112 | if ( $this->isMarkdownParsed() ) { 113 | return; 114 | } 115 | 116 | $markdown = App::make( 'markdown' )->convert( 117 | file_get_contents( $this->filepath() ) 118 | ); 119 | 120 | $this->content = $markdown->content(); 121 | $this->meta = $markdown->frontMatter(); 122 | $this->markdown_parsed = true; 123 | $this->yaml_parsed = true; 124 | } 125 | 126 | /** 127 | * Just-in-time YAML parsing. This should not be called unless 128 | * Markdown or YAML has yet to be parsed. 129 | * 130 | * @since 1.0.0 131 | */ 132 | protected function parseYaml(): void 133 | { 134 | if ( $this->isYamlParsed() ) { 135 | return; 136 | } 137 | 138 | $content = file_get_contents( 139 | $this->filepath(), false, null, 0, 4 * 1024 140 | ); 141 | 142 | $this->yaml_parsed = true; 143 | $this->meta = $content ? Str::frontMatter( $content ) : []; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Content/Entry/Virtual.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 13 | * @link https://github.com/blush-dev/framework 14 | * @license https://opensource.org/licenses/MIT 15 | */ 16 | 17 | namespace Blush\Content\Entry; 18 | 19 | use Blush\Core\Proxies\App; 20 | use Blush\Contracts\Content\ContentType; 21 | 22 | class Virtual extends Entry 23 | { 24 | /** 25 | * Sets up the object state. 26 | * 27 | * @since 1.0.0 28 | */ 29 | public function __construct( array $data = [] ) 30 | { 31 | foreach ( array_keys( get_object_vars( $this ) ) as $key ) { 32 | if ( isset( $data[ $key ] ) ) { 33 | $this->$key = $data[ $key ]; 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Returns the entry type. 40 | * 41 | * @since 1.0.0 42 | */ 43 | public function type(): ContentType 44 | { 45 | return App::get( 'content.types' )->get( 'virtual' ); 46 | } 47 | 48 | /** 49 | * Returns the entry name (slug). 50 | * 51 | * @since 1.0.0 52 | */ 53 | public function name(): string 54 | { 55 | return ''; 56 | } 57 | 58 | /** 59 | * Returns the entry URL. 60 | * 61 | * @since 1.0.0 62 | */ 63 | public function url(): string 64 | { 65 | return ''; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Content/Type/Component.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Content\Type; 13 | 14 | use Blush\Contracts\Bootable; 15 | use Blush\Contracts\Content\ContentTypes; 16 | 17 | class Component implements Bootable 18 | { 19 | /** 20 | * Sets up object state. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public function __construct( 25 | protected ContentTypes $registry, 26 | protected array $types 27 | ) {} 28 | 29 | /** 30 | * Registers content types on boot. 31 | * 32 | * @since 1.0.0 33 | */ 34 | public function boot(): void 35 | { 36 | // Registers user-configured content types. 37 | foreach ( $this->types as $name => $options ) { 38 | $this->registry->add( $name, $options ); 39 | } 40 | 41 | // Registers the virtual content type. 42 | $this->registry->add( 'virtual', [ 43 | 'public' => false, 44 | 'routing' => false 45 | ] ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Content/Type/Types.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Content\Type; 13 | 14 | use Blush\Core\Proxies\App; 15 | use Blush\Contracts\Content\{ContentType, ContentTypes}; 16 | use Blush\Tools\Collection; 17 | 18 | class Types extends Collection implements ContentTypes 19 | { 20 | /** 21 | * Stores types by path. 22 | * 23 | * @since 1.0.0 24 | */ 25 | private array $paths = []; 26 | 27 | /** 28 | * Stores types by URI. 29 | * 30 | * @since 1.0.0 31 | */ 32 | private array $uris = []; 33 | 34 | /** 35 | * Adds a custom content type. 36 | * 37 | * @since 1.0.0 38 | */ 39 | public function add( mixed $name, mixed $options = [] ): void 40 | { 41 | parent::add( $name, App::make( 'content.type', [ 42 | 'name' => $name, 43 | 'options' => $options 44 | ] ) ); 45 | } 46 | 47 | /** 48 | * Gets a custom content type by its path. 49 | * 50 | * @since 1.0.0 51 | */ 52 | public function getTypeFromPath( string $path ): ContentType|false 53 | { 54 | // If there is no path, this is a page. 55 | if ( '' === $path ) { 56 | return $this->get( 'page' ); 57 | } 58 | 59 | // If paths are not stored, loop through all and store them in 60 | // the `$paths` property. 61 | if ( ! $this->paths ) { 62 | foreach ( $this->all() as $type ) { 63 | $this->paths[ $type->path() ] = $type; 64 | } 65 | } 66 | 67 | // Return the type if the path matches. Else, false. 68 | return $this->paths[ $path ] ?? false; 69 | } 70 | 71 | /** 72 | * Gets a custom content type by its URI. 73 | * 74 | * @since 1.0.0 75 | */ 76 | public function getTypeFromUri( string $uri ): ContentType|false 77 | { 78 | // If there is no URI, this is a page. 79 | if ( '' === $uri ) { 80 | return $this->get( 'page' ); 81 | } 82 | 83 | // If URIs are not stored, loop through all and store them in 84 | // the `$uris` property. 85 | if ( ! $this->uris ) { 86 | foreach ( $this->all() as $type ) { 87 | $this->uris[ $type->urlPath() ] = $type; 88 | } 89 | } 90 | 91 | // Return the type if the URI matches. Else, false. 92 | return $this->uris[ $uri ] ?? false; 93 | } 94 | 95 | /** 96 | * Sorts types by their path. 97 | * 98 | * @since 1.0.0 99 | */ 100 | public function sortByPath(): array 101 | { 102 | $paths = []; 103 | $sorted = []; 104 | 105 | // Gets all the type paths. 106 | foreach ( $this->all() as $type ) { 107 | $paths[] = $type->path(); 108 | } 109 | 110 | // Sort paths alphabetically. 111 | asort( $paths ); 112 | 113 | // Loop through each of the paths and get its type. 114 | foreach ( $paths as $path ) { 115 | $sorted[] = $this->getTypeFromPath( $path ); 116 | } 117 | 118 | return $sorted; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Contracts/Bootable.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 13 | * @link https://github.com/blush-dev/framework 14 | * @license https://opensource.org/licenses/MIT 15 | */ 16 | 17 | namespace Blush\Contracts; 18 | 19 | interface Bootable 20 | { 21 | /** 22 | * Bootstraps the class. 23 | * 24 | * @since 1.0.0 25 | */ 26 | public function boot(): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Cache/Driver.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Cache; 13 | 14 | use Closure; 15 | 16 | interface Driver 17 | { 18 | /** 19 | * Returns the store name. 20 | * 21 | * @since 1.0.0 22 | */ 23 | public function store(): string; 24 | 25 | /** 26 | * Check if the store has data by cache key. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function has( string $key ): bool; 31 | 32 | /** 33 | * Returns data from a store by cache key. 34 | * 35 | * @since 1.0.0 36 | */ 37 | public function get( string $key ): mixed; 38 | 39 | /** 40 | * Writes new data or replaces existing data by cache key. 41 | * 42 | * @since 1.0.0 43 | */ 44 | public function put( string $key, mixed $data, int $seconds = 0 ): bool; 45 | 46 | /** 47 | * Writes new data if it doesn't exist by cache key. 48 | * 49 | * @since 1.0.0 50 | */ 51 | public function add( string $key, mixed $data, int $seconds = 0 ): bool; 52 | 53 | /** 54 | * Deletes data if it exists by cache key. 55 | * 56 | * @since 1.0.0 57 | */ 58 | public function forget( string $key ): bool; 59 | 60 | /** 61 | * Writes new data if it doesn't exist by cache key. Doesn't expire. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function forever( string $key, $data ): bool; 66 | 67 | /** 68 | * Gets and returns data by cache key. If it doesn't exist, callback is 69 | * executed to pass in custom data and write it. 70 | * 71 | * @since 1.0.0 72 | */ 73 | public function remember( string $key, int $seconds, Closure $callback ): mixed; 74 | 75 | /** 76 | * Gets and returns data by cache key. If it doesn't exist, callback is 77 | * executed to pass in custom data and write it. Doesn't expire. 78 | * 79 | * @since 1.0.0 80 | */ 81 | public function rememberForever( string $key, Closure $callback ): mixed; 82 | 83 | /** 84 | * Deletes all cached data from a store. 85 | * 86 | * @since 1.0.0 87 | */ 88 | public function flush(): void; 89 | 90 | /** 91 | * Gets and returns data by key. Deletes previous data. 92 | * 93 | * @since 1.0.0 94 | */ 95 | public function pull( string $key ): mixed; 96 | 97 | /** 98 | * Returns the timestamp for when a dataset was created. 99 | * 100 | * @since 1.0.0 101 | */ 102 | public function created( string $key ): ?int; 103 | 104 | /** 105 | * Returns the timestamp for when a dataset expires. 106 | * 107 | * @since 1.0.0 108 | */ 109 | public function expires( string $key ): ?int; 110 | 111 | /** 112 | * Determines if a dataset has expired. 113 | * 114 | * @since 1.0.0 115 | */ 116 | public function expired( string $key ): bool; 117 | } 118 | -------------------------------------------------------------------------------- /src/Contracts/Cache/Registry.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Cache; 13 | 14 | use Closure; 15 | 16 | interface Registry 17 | { 18 | /** 19 | * Returns a store driver object or `false`. 20 | * 21 | * @since 1.0.0 22 | */ 23 | public function store( string $store ): Driver|false; 24 | 25 | /** 26 | * Returns all stores. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function getStores(): array; 31 | 32 | /** 33 | * Adds a store. 34 | * 35 | * @since 1.0.0 36 | */ 37 | public function addStore( string $name, array $options = [] ): void; 38 | 39 | /** 40 | * Removes a store. 41 | * 42 | * @since 1.0.0 43 | */ 44 | public function removeStore( string $store ): void; 45 | 46 | /** 47 | * Checks if a store exists. 48 | * 49 | * @since 1.0.0 50 | */ 51 | public function storeExists( string $store ): bool; 52 | 53 | /** 54 | * Returns a driver. 55 | * 56 | * @since 1.0.0 57 | */ 58 | public function driver( string $name ): string|false; 59 | 60 | /** 61 | * Checks if a driver exists. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function driverExists( string $name ): bool; 66 | 67 | /** 68 | * Adds a driver. 69 | * 70 | * @since 1.0.0 71 | */ 72 | public function addDriver( string $name, string $driver ): void; 73 | 74 | /** 75 | * Removes a driver. 76 | * 77 | * @since 1.0.0 78 | */ 79 | public function removeDriver( string $name ): void; 80 | 81 | /** 82 | * Check if the store has data via `store.key`. 83 | * 84 | * @since 1.0.0 85 | */ 86 | public function has( string $name ): bool; 87 | 88 | /** 89 | * Returns data from a store via `store.key`. 90 | * 91 | * @since 1.0.0 92 | */ 93 | public function get( string $name ): mixed; 94 | 95 | /** 96 | * Writes new data or replaces existing data via `store.key`. 97 | * 98 | * @since 1.0.0 99 | */ 100 | public function put( string $name, mixed $data, int $seconds = 0 ): bool; 101 | 102 | /** 103 | * Writes new data if it doesn't exist via `store.key`. 104 | * 105 | * @since 1.0.0 106 | */ 107 | public function add( string $name, $data, int $seconds = 0 ): void; 108 | 109 | /** 110 | * Deletes data if it exists via `store.key`. 111 | * 112 | * @since 1.0.0 113 | */ 114 | public function forget( string $name ): void; 115 | 116 | /** 117 | * Writes new data if it doesn't exist via `store.key`. Doesn't expire. 118 | * 119 | * @since 1.0.0 120 | */ 121 | public function forever( string $name, $data ): void; 122 | 123 | /** 124 | * Gets and returns data via `store.key`. If it doesn't exist, callback 125 | * is executed to pass in custom data and write it. 126 | * 127 | * @since 1.0.0 128 | */ 129 | public function remember( string $name, int $seconds, Closure $callback ): mixed; 130 | 131 | /** 132 | * Gets and returns data via `store.key`. If it doesn't exist, callback 133 | * is executed to pass in custom data and write it. Doesn't expire. 134 | * 135 | * @since 1.0.0 136 | */ 137 | public function rememberForever( string $name, Closure $callback ): mixed; 138 | 139 | /** 140 | * Gets and returns data via `store.key`. Deletes previous data. 141 | * 142 | * @since 1.0.0 143 | */ 144 | public function pull( string $name ): mixed; 145 | 146 | /** 147 | * Returns the timestamp for when a dataset was created via `store.key`. 148 | * 149 | * @since 1.0.0 150 | */ 151 | public function created( string $name ): ?int; 152 | 153 | /** 154 | * Returns the timestamp for when a dataset expires via `store.key`. 155 | * 156 | * @since 1.0.0 157 | */ 158 | public function expires( string $name ): ?int; 159 | 160 | /** 161 | * Determines if a dataset has expired via `store.key`. 162 | * 163 | * @since 1.0.0 164 | */ 165 | public function expired( string $name ): bool; 166 | 167 | /** 168 | * Deletes all cached data from a store. 169 | * 170 | * @since 1.0.0 171 | */ 172 | public function flush( string $store ): void; 173 | 174 | /** 175 | * Flushes the cached data from all stores. 176 | * 177 | * @since 1.0.0 178 | */ 179 | public function purge(): void; 180 | } 181 | -------------------------------------------------------------------------------- /src/Contracts/CastsToHtml.php: -------------------------------------------------------------------------------- 1 | 14 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 15 | * @link https://github.com/blush-dev/framework 16 | * @license https://opensource.org/licenses/MIT 17 | */ 18 | 19 | namespace Blush\Contracts; 20 | 21 | interface CastsToHtml 22 | { 23 | /** 24 | * Returns an HTML string for output. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function toHtml(): string; 29 | } 30 | -------------------------------------------------------------------------------- /src/Contracts/CastsToText.php: -------------------------------------------------------------------------------- 1 | 14 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 15 | * @link https://github.com/blush-dev/framework 16 | * @license https://opensource.org/licenses/MIT 17 | */ 18 | 19 | namespace Blush\Contracts; 20 | 21 | interface CastsToText 22 | { 23 | /** 24 | * Returns an HTML string for output. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function toText(): string; 29 | } 30 | -------------------------------------------------------------------------------- /src/Contracts/Content/ContentEntry.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 10 | * @link https://github.com/blush-dev/framework 11 | * @license https://opensource.org/licenses/MIT 12 | */ 13 | 14 | namespace Blush\Contracts\Content; 15 | 16 | use Blush\Tools\Media; 17 | 18 | interface ContentEntry 19 | { 20 | /** 21 | * Returns the entry type. 22 | * 23 | * @since 1.0.0 24 | */ 25 | public function type(): ContentType; 26 | 27 | /** 28 | * Returns the entry name/slug/ID. 29 | * 30 | * @since 1.0.0 31 | */ 32 | public function name(): string; 33 | 34 | /** 35 | * Returns the entry's visibility. 36 | * 37 | * @since 1.0.0 38 | */ 39 | public function visibility(): string; 40 | 41 | /** 42 | * Checks if an entry is viewable to the public. 43 | * 44 | * @since 1.0.0 45 | */ 46 | public function isPublic(): bool; 47 | 48 | /** 49 | * Checks if an entry is hidden from the public. 50 | * 51 | * @since 1.0.0 52 | */ 53 | public function isHidden(): bool; 54 | 55 | /** 56 | * Returns the entry URL. 57 | * 58 | * @since 1.0.0 59 | */ 60 | public function url(): string; 61 | 62 | /** 63 | * Returns the entry content. 64 | * 65 | * @since 1.0.0 66 | */ 67 | public function content(): string; 68 | 69 | /** 70 | * Returns entry metadata. 71 | * 72 | * @since 1.0.0 73 | */ 74 | public function meta( string $name = '', mixed $default = false ): mixed; 75 | 76 | /** 77 | * Returns only a single meta value. 78 | * 79 | * @since 1.0.0 80 | */ 81 | public function metaSingle( string $name, mixed $default = false ): mixed; 82 | 83 | /** 84 | * Ensures that an array of meta values is returned. 85 | * 86 | * @since 1.0.0 87 | */ 88 | public function metaArr( string $name, array $default = [] ): array; 89 | 90 | /** 91 | * Returns a Query for content type entries stored in the current 92 | * entry's metadata. 93 | * 94 | * @since 1.0.0 95 | */ 96 | public function metaQuery( string $name, array $args = [] ): ContentQuery|false; 97 | 98 | /** 99 | * Returns the entry title. 100 | * 101 | * @since 1.0.0 102 | */ 103 | public function title(): string; 104 | 105 | /** 106 | * Returns the entry subtitle. 107 | * 108 | * @since 1.0.0 109 | */ 110 | public function subtitle(): string; 111 | 112 | /** 113 | * Returns the entry published datetime. 114 | * 115 | * @since 1.0.0 116 | */ 117 | public function published( string $format = '' ): string; 118 | 119 | /** 120 | * Returns the entry updated datetime. 121 | * 122 | * @since 1.0.0 123 | */ 124 | public function updated( string $format = '' ): string; 125 | 126 | /** 127 | * Returns the entry date. 128 | * 129 | * @since 1.0.0 130 | * @deprecated 1.0.0 131 | */ 132 | public function date(): string; 133 | 134 | /** 135 | * Returns the entry author. 136 | * 137 | * @since 1.0.0 138 | */ 139 | public function author(): ContentEntry|false; 140 | 141 | /** 142 | * Returns the entry authors. 143 | * 144 | * @since 1.0.0 145 | */ 146 | public function authors(): array; 147 | 148 | /** 149 | * Returns a media object based on a media file path stored as metadata. 150 | * 151 | * @since 1.0.0 152 | */ 153 | public function media( string $name = 'image' ): Media|null; 154 | 155 | /** 156 | * Returns an array of view paths assigned as metadata. 157 | * 158 | * @since 1.0.0 159 | */ 160 | public function viewPaths(): array; 161 | 162 | /** 163 | * Returns an array of Query arguments if assigned as metadata. 164 | * 165 | * @since 1.0.0 166 | */ 167 | public function collectionArgs(): array; 168 | 169 | /** 170 | * Returns an array of the taxonomy (content type) objects associated 171 | * with the entry. 172 | * 173 | * @since 1.0.0 174 | */ 175 | public function taxonomies(): array; 176 | 177 | /** 178 | * Conditional check if the entry is associated with a taxonomy. 179 | * 180 | * @since 1.0.0 181 | */ 182 | public function hasTaxonomy( string $taxonomy ): bool; 183 | 184 | /** 185 | * Returns a Query of taxonomy entries or false. 186 | * 187 | * @since 1.0.0 188 | */ 189 | public function terms( string $taxonomy, array $args = [] ): ContentQuery|false; 190 | 191 | /** 192 | * Conditional check if the entry has a term from a specific taxonomy. 193 | * 194 | * @since 1.0.0 195 | */ 196 | public function hasTerm( string $taxonomy, string $term ): bool; 197 | 198 | /** 199 | * Returns the entry excerpt. 200 | * 201 | * @since 1.0.0 202 | */ 203 | public function excerpt( int $limit = 50, string $more = '…' ): string; 204 | 205 | /** 206 | * Returns an estimated reading time in hours (if an hour or longer) and 207 | * minutes. 208 | * 209 | * @since 1.0.0 210 | */ 211 | public function readingTime( int $words_per_min = 200 ): string; 212 | } 213 | -------------------------------------------------------------------------------- /src/Contracts/Content/ContentLocator.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 10 | * @link https://github.com/blush-dev/framework 11 | * @license https://opensource.org/licenses/MIT 12 | */ 13 | 14 | namespace Blush\Contracts\Content; 15 | 16 | interface ContentLocator 17 | { 18 | /** 19 | * Sets the locator path. 20 | * 21 | * @since 1.0.0 22 | */ 23 | public function setPath( string $path ): void; 24 | 25 | /** 26 | * Returns the folder path relative to the content directory. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function path(): string; 31 | 32 | /** 33 | * Returns collection of located files as an array. The filenames are 34 | * the array keys and the metadata is the value. 35 | * 36 | * @since 1.0.0 37 | */ 38 | public function all(): array; 39 | } 40 | -------------------------------------------------------------------------------- /src/Contracts/Content/ContentQuery.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 10 | * @link https://github.com/blush-dev/framework 11 | * @license https://opensource.org/licenses/MIT 12 | */ 13 | 14 | namespace Blush\Contracts\Content; 15 | 16 | interface ContentQuery 17 | { 18 | /** 19 | * Returns the located entries as an array. 20 | * 21 | * @since 1.0.0 22 | */ 23 | public function all(): array; 24 | 25 | /** 26 | * Checks if the query has any entries. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function hasEntries(): bool; 31 | 32 | /** 33 | * Checks if an entry was located by slug. 34 | * 35 | * @since 1.0.0 36 | */ 37 | public function has( string $slug ): bool; 38 | 39 | /** 40 | * Returns the first entry. Alias for `first()`. 41 | * 42 | * @since 1.0.0 43 | */ 44 | public function single(): ?ContentEntry; 45 | 46 | /** 47 | * Returns the first entry. 48 | * 49 | * @since 1.0.0 50 | */ 51 | public function first(): ?ContentEntry; 52 | 53 | /** 54 | * Returns the last entry. 55 | * 56 | * @since 1.0.0 57 | */ 58 | public function last(): ?ContentEntry; 59 | 60 | /** 61 | * Returns the count for the current query. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function count(): int; 66 | 67 | /** 68 | * Returns the total entries. 69 | * 70 | * @since 1.0.0 71 | */ 72 | public function total(): int; 73 | 74 | /** 75 | * Returns the number query option. 76 | * 77 | * @since 1.0.0 78 | */ 79 | public function number(): int; 80 | 81 | /** 82 | * Returns the number of pages of entries. 83 | * 84 | * @since 1.0.0 85 | */ 86 | public function pages(): int; 87 | 88 | /** 89 | * Returns the offset query option. 90 | * 91 | * @since 1.0.0 92 | */ 93 | public function offset(): int; 94 | } 95 | -------------------------------------------------------------------------------- /src/Contracts/Content/ContentType.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Content; 13 | 14 | interface ContentType 15 | { 16 | /** 17 | * Returns the content type name. 18 | * 19 | * @since 1.0.0 20 | */ 21 | public function name(): string; 22 | 23 | /** 24 | * Returns the content type name (alias for `name()`). 25 | * 26 | * @since 1.0.0 27 | * @deprecated 1.0.0 28 | */ 29 | public function type(): string; 30 | 31 | /** 32 | * Conditional check if the typs is the homepage alias. 33 | * 34 | * @since 1.0.0 35 | */ 36 | public function isHomeAlias(): bool; 37 | 38 | /** 39 | * Returns the content type path. 40 | * 41 | * @since 1.0.0 42 | */ 43 | public function path(): string; 44 | 45 | /** 46 | * Returns a keyed URL path. 47 | * 48 | * @since 1.0.0 49 | */ 50 | public function urlPath( string $key = '' ): string; 51 | 52 | /** 53 | * Returns the content type URL. 54 | * 55 | * @since 1.0.0 56 | */ 57 | public function url(): string; 58 | 59 | /** 60 | * Returns the content type URL for single entries. 61 | * 62 | * @since 1.0.0 63 | */ 64 | public function singleUrl( array $params = [] ): string; 65 | 66 | /** 67 | * Returns the content type feed URL. 68 | * 69 | * @since 1.0.0 70 | */ 71 | public function feedUrl(): string; 72 | 73 | /** 74 | * Returns the content type RSS feed URL. 75 | * 76 | * @since 1.0.0 77 | */ 78 | public function rssFeedUrl(): string; 79 | 80 | /** 81 | * Returns the content type atom feed URL. 82 | * 83 | * @since 1.0.0 84 | */ 85 | public function atomFeedUrl(): string; 86 | 87 | /** 88 | * Returns the content type URL for year archives. 89 | * 90 | * @since 1.0.0 91 | */ 92 | public function yearUrl( string $year ): string; 93 | 94 | /** 95 | * Returns the content type URL for month archives. 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function monthUrl( string $year, string $month ): string; 100 | 101 | /** 102 | * Returns the content type URI for day archives. 103 | * 104 | * @since 1.0.0 105 | */ 106 | public function dayUrl( string $year, string $month, string $day ): string; 107 | 108 | /** 109 | * Whether the content type should have a feed. 110 | * 111 | * @since 1.0.0 112 | */ 113 | public function hasFeed(): bool; 114 | 115 | /** 116 | * Whether date archives are supported. If time archives are supported 117 | * date archives are required to be enabled. 118 | * 119 | * @since 1.0.0 120 | */ 121 | public function hasDateArchives(): bool; 122 | 123 | /** 124 | * Whether time archives are supported. 125 | * 126 | * @since 1.0.0 127 | */ 128 | public function hasTimeArchives(): bool; 129 | 130 | /** 131 | * Whether routing is enabled. 132 | * 133 | * @since 1.0.0 134 | */ 135 | public function routing(): array|bool; 136 | 137 | /** 138 | * Returns the type this content type collects. 139 | * 140 | * @since 1.0.0 141 | */ 142 | public function collect(): string|bool|null; 143 | 144 | /** 145 | * Returns the type that terms of this type collects if a taxonomy. 146 | * 147 | * @since 1.0.0 148 | */ 149 | public function termCollect(): string|bool|null; 150 | 151 | /** 152 | * Returns an array of Query arguments when the content type is used in 153 | * a collection. 154 | * 155 | * @since 1.0.0 156 | */ 157 | public function collectionArgs(): array; 158 | 159 | /** 160 | * Returns an array of Query arguments when the terms of the content 161 | * type is called as a collection. Only works for taxonomies. 162 | * 163 | * @since 1.0.0 164 | */ 165 | public function termCollectionArgs(): array; 166 | 167 | /** 168 | * Returns an array of Query arguments when the content type is used in 169 | * a feed. 170 | * 171 | * @since 1.0.0 172 | */ 173 | public function feedArgs(): array; 174 | 175 | /** 176 | * Whether this type is a taxonomy. 177 | * 178 | * @since 1.0.0 179 | */ 180 | public function isTaxonomy(): bool; 181 | 182 | /** 183 | * Returns the content type routes as an array. 184 | * 185 | * @since 1.0.0 186 | */ 187 | public function routes(): array; 188 | } 189 | -------------------------------------------------------------------------------- /src/Contracts/Content/ContentTypes.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Content; 13 | 14 | interface ContentTypes 15 | { 16 | /** 17 | * Gets a custom content type by its path. 18 | * 19 | * @since 1.0.0 20 | */ 21 | public function getTypeFromPath( string $path ): ContentType|false; 22 | 23 | /** 24 | * Gets a custom content type by its URI. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function getTypeFromUri( string $uri ): ContentType|false; 29 | 30 | /** 31 | * Sorts types by their path. 32 | * 33 | * @since 1.0.0 34 | */ 35 | public function sortByPath(): array; 36 | } 37 | -------------------------------------------------------------------------------- /src/Contracts/Core/Application.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Core; 13 | 14 | /** 15 | * Application interface. 16 | * 17 | * @since 1.0.0 18 | */ 19 | interface Application extends Container 20 | { 21 | /** 22 | * Access a keyed path and append a path to it. 23 | * 24 | * @since 1.0.0 25 | */ 26 | public function path( string $accessor = '', string $append = '' ): string; 27 | 28 | /** 29 | * Returns app path with optional appended path/file. 30 | * 31 | * @since 1.0.0 32 | */ 33 | public function appPath( string $append = '' ): string; 34 | 35 | /** 36 | * Returns config path with optional appended path/file. 37 | * 38 | * @since 1.0.0 39 | */ 40 | public function configPath( string $append = '' ): string; 41 | 42 | /** 43 | * Returns public path with optional appended path/file. 44 | * 45 | * @since 1.0.0 46 | */ 47 | public function publicPath( string $append = '' ): string; 48 | 49 | /** 50 | * Returns view path with optional appended path/file. 51 | * 52 | * @since 1.0.0 53 | */ 54 | public function viewPath( string $append = '' ): string; 55 | 56 | /** 57 | * Returns resource path with optional appended path/file. 58 | * 59 | * @since 1.0.0 60 | */ 61 | public function resourcePath( string $append = '' ): string; 62 | 63 | /** 64 | * Returns storage path with optional appended path/file. 65 | * 66 | * @since 1.0.0 67 | */ 68 | public function storagePath( string $append = '' ): string; 69 | 70 | /** 71 | * Returns cache path with optional appended path/file. 72 | * 73 | * @since 1.0.0 74 | */ 75 | public function cachePath( string $append = '' ): string; 76 | 77 | /** 78 | * Returns user path with optional appended path/file. 79 | * 80 | * @since 1.0.0 81 | */ 82 | public function userPath( string $append = '' ): string; 83 | 84 | /** 85 | * Returns content path with optional appended path/file. 86 | * 87 | * @since 1.0.0 88 | */ 89 | public function contentPath( string $append = '' ): string; 90 | 91 | /** 92 | * Returns media path with optional appended path/file. 93 | * 94 | * @since 1.0.0 95 | */ 96 | public function mediaPath( string $append = '' ): string; 97 | 98 | /** 99 | * Returns vendor path with optional appended path/file. 100 | * 101 | * @since 1.0.0 102 | */ 103 | public function vendorPath( string $append = '' ): string; 104 | 105 | /** 106 | * Access a keyed URL and append a path to it. 107 | * 108 | * @since 1.0.0 109 | */ 110 | public function url( string $accessor = '', string $append = '' ): string; 111 | 112 | /** 113 | * Returns app URL with optional appended path/file. 114 | * 115 | * @since 1.0.0 116 | */ 117 | public function appUrl( string $append = '' ): string; 118 | 119 | /** 120 | * Returns config URL with optional appended path/file. 121 | * 122 | * @since 1.0.0 123 | */ 124 | public function configUrl( string $append = '' ): string; 125 | 126 | /** 127 | * Returns public URL with optional appended path/file. 128 | * 129 | * @since 1.0.0 130 | */ 131 | public function publicUrl( string $append = '' ): string; 132 | 133 | /** 134 | * Returns view URL with optional appended path/file. 135 | * 136 | * @since 1.0.0 137 | */ 138 | public function viewUrl( string $append = '' ): string; 139 | 140 | /** 141 | * Returns resource URL with optional appended path/file. 142 | * 143 | * @since 1.0.0 144 | */ 145 | public function resourceUrl( string $append = '' ): string; 146 | 147 | /** 148 | * Returns storage URL with optional appended path/file. 149 | * 150 | * @since 1.0.0 151 | */ 152 | public function storageUrl( string $append = '' ): string; 153 | 154 | /** 155 | * Returns cache URL with optional appended path/file. 156 | * 157 | * @since 1.0.0 158 | */ 159 | public function cacheUrl( string $append = '' ): string; 160 | 161 | /** 162 | * Returns user URL with optional appended path/file. 163 | * 164 | * @since 1.0.0 165 | */ 166 | public function userUrl( string $append = '' ): string; 167 | 168 | /** 169 | * Returns content URL with optional appended path/file. 170 | * 171 | * @since 1.0.0 172 | */ 173 | public function contentUrl( string $append = '' ): string; 174 | 175 | /** 176 | * Returns media URL with optional appended path/file. 177 | * 178 | * @since 1.0.0 179 | */ 180 | public function mediaUrl( string $append = '' ): string; 181 | 182 | /** 183 | * Returns vendor URL with optional appended path/file. 184 | * 185 | * @since 1.0.0 186 | */ 187 | public function vendorUrl( string $append = '' ): string; 188 | } 189 | -------------------------------------------------------------------------------- /src/Contracts/Core/Container.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 11 | * @link https://github.com/blush-dev/framework 12 | * @license https://opensource.org/licenses/MIT 13 | */ 14 | 15 | namespace Blush\Contracts\Core; 16 | 17 | use Closure; 18 | use Blush\Core\ServiceProvider; 19 | 20 | interface Container 21 | { 22 | /** 23 | * Add a binding. The abstract should be a key, abstract class name, or 24 | * interface name. The concrete should be the concrete implementation of 25 | * the abstract. 26 | * 27 | * @since 1.0.0 28 | */ 29 | public function bind( 30 | string $abstract, 31 | mixed $concrete = null, 32 | bool $shared = false 33 | ): void; 34 | 35 | /** 36 | * Alias for `bind()`. 37 | * 38 | * @since 1.0.0 39 | */ 40 | public function add( 41 | string $abstract, 42 | mixed $concrete = null, 43 | bool $shared = false 44 | ): void; 45 | 46 | /** 47 | * Remove a binding. 48 | * 49 | * @since 1.0.0 50 | */ 51 | public function remove( string $abstract ): void; 52 | 53 | /** 54 | * Resolve and return the binding. 55 | * 56 | * @since 1.0.0 57 | */ 58 | public function resolve( string $abstract, array $parameters = [] ): mixed; 59 | 60 | /** 61 | * Alias for `resolve()`. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function make( string $abstract, array $parameters = [] ): mixed; 66 | 67 | /** 68 | * Alias for `resolve()`. 69 | * 70 | * Follows the PSR-11 standard. Do not alter. 71 | * @link https://www.php-fig.org/psr/psr-11/ 72 | * 73 | * @since 1.0.0 74 | */ 75 | public function get( string $abstract ): mixed; 76 | 77 | /** 78 | * Check if a binding exists. 79 | * 80 | * Follows the PSR-11 standard. Do not alter. 81 | * @link https://www.php-fig.org/psr/psr-11/ 82 | * 83 | * @since 1.0.0 84 | */ 85 | public function has( string $abstract ): bool; 86 | 87 | /** 88 | * Add a shared binding. 89 | * 90 | * @since 1.0.0 91 | */ 92 | public function singleton( string $abstract, mixed $concrete = null ): void; 93 | 94 | /** 95 | * Add an existing instance. 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function instance( string $abstract, mixed $instance ): mixed; 100 | 101 | /** 102 | * Extend a binding. 103 | * 104 | * @since 1.0.0 105 | */ 106 | public function extend( string $abstract, Closure $closure ): void; 107 | 108 | /** 109 | * Create an alias for an abstract type. 110 | * 111 | * @since 1.0.0 112 | */ 113 | public function alias( string $abstract, string $alias ): void; 114 | 115 | /** 116 | * Adds a service provider. Developers can pass in an object or a fully- 117 | * qualified class name. 118 | * 119 | * @since 1.0.0 120 | */ 121 | public function provider( ServiceProvider|string $provider ): void; 122 | 123 | /** 124 | * Adds a static proxy alias. Developers must pass in fully-qualified 125 | * class name and alias class name. 126 | * 127 | * @since 1.0.0 128 | */ 129 | public function proxy( string $class_name, string $alias ): void; 130 | } 131 | -------------------------------------------------------------------------------- /src/Contracts/Displayable.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 12 | * @link https://github.com/blush-dev/framework 13 | * @license https://opensource.org/licenses/MIT 14 | */ 15 | 16 | namespace Blush\Contracts; 17 | 18 | interface Displayable 19 | { 20 | /** 21 | * Prints the HTML string. 22 | * 23 | * @since 1.0.0 24 | * @access public 25 | */ 26 | public function display(): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Feed/FeedWriter.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Feed; 13 | 14 | use Blush\Contracts\Content\ContentQuery; 15 | 16 | interface FeedWriter 17 | { 18 | /** 19 | * Returns the Feed title. 20 | * 21 | * @since 1.0.0 22 | */ 23 | public function title(): string; 24 | 25 | /** 26 | * Returns the Feed webpage URL. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function url(): string; 31 | 32 | /** 33 | * Returns the Feed feed URL. 34 | * 35 | * @since 1.0.0 36 | */ 37 | public function feedUrl(): string; 38 | 39 | /** 40 | * Returns the Feed description. 41 | * 42 | * @since 1.0.0 43 | */ 44 | public function description(): string; 45 | 46 | /** 47 | * Returns the Feed language. 48 | * 49 | * @since 1.0.0 50 | */ 51 | public function language(): string; 52 | 53 | /** 54 | * Returns the Feed TTL. 55 | * 56 | * @since 1.0.0 57 | */ 58 | public function copyright(): ?string; 59 | 60 | /** 61 | * Returns the feed published datetime. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function published(): ?string; 66 | 67 | /** 68 | * Returns the feed updated datetime. 69 | * 70 | * @since 1.0.0 71 | */ 72 | public function updated(): ?string; 73 | 74 | /** 75 | * Returns the Feed TTL. 76 | * 77 | * @since 1.0.0 78 | */ 79 | public function ttl(): int; 80 | 81 | /** 82 | * Returns the collection. 83 | * 84 | * @since 1.0.0 85 | */ 86 | public function collection(): ContentQuery; 87 | } 88 | -------------------------------------------------------------------------------- /src/Contracts/Makeable.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 12 | * @link https://github.com/blush-dev/framework 13 | * @license https://opensource.org/licenses/MIT 14 | */ 15 | 16 | namespace Blush\Contracts; 17 | 18 | interface Makeable 19 | { 20 | /** 21 | * Makes an object. 22 | * 23 | * @since 1.0.0 24 | */ 25 | public function make(): self; 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/Markdown/Parser.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Markdown; 13 | 14 | interface Parser 15 | { 16 | /** 17 | * Converts Markdown to HTML. 18 | * 19 | * @since 1.0.0 20 | */ 21 | public function convert( string $content ): self; 22 | 23 | /** 24 | * Returns Markdown HTML. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function content(): string; 29 | 30 | /** 31 | * Returns YAML front matter. 32 | * 33 | * @since 1.0.0 34 | */ 35 | public function frontMatter(): array; 36 | } 37 | -------------------------------------------------------------------------------- /src/Contracts/Renderable.php: -------------------------------------------------------------------------------- 1 | 14 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 15 | * @link https://github.com/blush-dev/framework 16 | * @license https://opensource.org/licenses/MIT 17 | */ 18 | 19 | namespace Blush\Contracts; 20 | 21 | interface Renderable 22 | { 23 | /** 24 | * Returns an HTML string for output. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function render(): string; 29 | } 30 | -------------------------------------------------------------------------------- /src/Contracts/Routing/RoutingRoute.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Routing; 13 | 14 | use Blush\Controllers\Controller; 15 | use Symfony\Component\HttpFoundation\{Request, Response}; 16 | 17 | interface RoutingRoute 18 | { 19 | /** 20 | * Returns the route URI. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public function uri(): string; 25 | 26 | /** 27 | * Assigns the route name and returns self for chaining. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function name( string $name ): self; 32 | 33 | /** 34 | * Returns the route name. 35 | * 36 | * @since 1.0.0 37 | */ 38 | public function getName(): string; 39 | 40 | /** 41 | * Returns the route controller. 42 | * 43 | * @since 1.0.0 44 | */ 45 | public function controller(): Controller|string; 46 | 47 | /** 48 | * Invokes the route controller. 49 | * 50 | * @since 1.0.0 51 | */ 52 | public function callback( array $params, Request $request ): Response; 53 | 54 | /** 55 | * Returns the route regex pattern. 56 | * 57 | * @since 1.0.0 58 | */ 59 | public function pattern(): string; 60 | 61 | /** 62 | * Returns the route parameters. 63 | * 64 | * @since 1.0.0 65 | */ 66 | public function parameters(): array; 67 | 68 | /** 69 | * Returns the route wheres. 70 | * 71 | * @since 1.0.0 72 | */ 73 | public function wheres(): array; 74 | 75 | /** 76 | * Add custom param to regex mapping. 77 | * 78 | * @since 1.0.0 79 | */ 80 | public function where( string|array $name, ?string $regex = null ): void; 81 | 82 | /** 83 | * Checks if a where param has been added. 84 | * 85 | * @since 1.0.0 86 | */ 87 | public function hasWhere( string $name ): bool; 88 | 89 | /** 90 | * Adds parameters to wheres with slug-based regex pattern. 91 | * 92 | * @since 1.0.0 93 | */ 94 | public function whereSlug( string|array $parameters ): void; 95 | 96 | /** 97 | * Adds parameters to wheres with alpha-based regex pattern. 98 | * 99 | * @since 1.0.0 100 | */ 101 | public function whereAlpha( string|array $parameters ): void; 102 | 103 | /** 104 | * Adds parameters to wheres with alphanumeric-based regex pattern. 105 | * 106 | * @since 1.0.0 107 | */ 108 | public function whereAlphaNumeric( string|array $parameters ): void; 109 | 110 | /** 111 | * Adds parameters to wheres with number-based regex pattern. 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function whereNumber( string|array $parameters ): void; 116 | 117 | /** 118 | * Adds parameters to wheres with year-based regex pattern. 119 | * 120 | * @since 1.0.0 121 | */ 122 | public function whereYear( string|array $parameters ): void; 123 | 124 | /** 125 | * Adds parameters to wheres with month-based regex pattern. 126 | * 127 | * @since 1.0.0 128 | */ 129 | public function whereMonth( string|array $parameters ): void; 130 | 131 | /** 132 | * Adds parameters to wheres with day-based regex pattern. 133 | * 134 | * @since 1.0.0 135 | */ 136 | public function whereDay( string|array $parameters ): void; 137 | } 138 | -------------------------------------------------------------------------------- /src/Contracts/Routing/RoutingRouter.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Routing; 13 | 14 | use Symfony\Component\HttpFoundation\{Request, Response}; 15 | 16 | interface RoutingRouter 17 | { 18 | /** 19 | * Returns the HTTP request. 20 | * 21 | * @since 1.0.0 22 | */ 23 | public function request(): Request; 24 | 25 | /** 26 | * Returns the request path. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function path(): string; 31 | 32 | /** 33 | * Returns a cached HTTP Response if global caching is enabled. If not, 34 | * returns a new HTTP Response. 35 | * 36 | * @since 1.0.0 37 | */ 38 | public function response(): Response; 39 | } 40 | -------------------------------------------------------------------------------- /src/Contracts/Routing/RoutingRoutes.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Routing; 13 | 14 | interface RoutingRoutes 15 | { 16 | /** 17 | * Returns route by name. 18 | * 19 | * @since 1.0.0 20 | */ 21 | public function getNamedRoute( string $name ): ?RoutingRoute; 22 | 23 | /** 24 | * Returns an array of all routes with their names as the keys and the 25 | * Route objects as the values. 26 | * 27 | * @since 1.0.0 28 | */ 29 | public function getRoutesByName(): array; 30 | } 31 | -------------------------------------------------------------------------------- /src/Contracts/Routing/RoutingUrl.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Routing; 13 | 14 | interface RoutingUrl 15 | { 16 | /** 17 | * Return the app URL and append an optional path. 18 | * 19 | * @since 1.0.0 20 | */ 21 | public function to( string $append = '' ): string; 22 | 23 | /** 24 | * Returns a route's URL. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function route( string $name, array $params = [] ): string; 29 | 30 | /** 31 | * Accepts a path or URL string with possible `{param}` values in it. 32 | * Replaces the `{param}` strings with values from the `$params` array. 33 | * 34 | * @since 1.0.0 35 | */ 36 | public function parseParams( string $path, array $params = [] ): string; 37 | } 38 | -------------------------------------------------------------------------------- /src/Contracts/Template/TemplateEngine.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Template; 13 | 14 | use Blush\Tools\Collection; 15 | 16 | interface TemplateEngine 17 | { 18 | /** 19 | * Returns a View object. This should only be used for top-level views 20 | * because it resets the shared data when called. If including views 21 | * within views, use `subview()` or descendent functions. 22 | * 23 | * @since 1.0.0 24 | */ 25 | public function view( 26 | array|string $views, 27 | array|Collection $data = [] 28 | ): TemplateView; 29 | 30 | /** 31 | * Checks if a view template exists. 32 | * 33 | * @since 1.0.0 34 | */ 35 | public function exists( string $name ): bool; 36 | 37 | /** 38 | * Returns the first found view. 39 | * 40 | * @since 1.0.0 41 | */ 42 | public function first( 43 | array $views, 44 | array|Collection $data = [] 45 | ): TemplateView; 46 | 47 | /** 48 | * Returns any found view. 49 | * 50 | * @since 1.0.0 51 | */ 52 | public function any( 53 | array $views, 54 | array|Collection $data = [] 55 | ): TemplateView|false; 56 | 57 | /** 58 | * Includes a subview. 59 | * 60 | * @since 1.0.0 61 | */ 62 | public function include( 63 | array|string $paths, 64 | array|Collection $data = [] 65 | ): void; 66 | 67 | /** 68 | * Includes a subview only if it exists. No errors or warnings if no 69 | * view template is found. 70 | * 71 | * @since 1.0.0 72 | */ 73 | public function includeIf( 74 | array|string $paths, 75 | array|Collection $data = [] 76 | ): void; 77 | 78 | /** 79 | * Includes a subview when `$when` is `true`. 80 | * 81 | * @since 1.0.0 82 | */ 83 | public function includeWhen( 84 | mixed $when, 85 | array|string $paths, 86 | array|Collection $data = [] 87 | ): void; 88 | 89 | /** 90 | * Includes a subview unless `$unless` is `true`. 91 | * 92 | * @since 1.0.0 93 | */ 94 | public function includeUnless( 95 | mixed $unless, 96 | array|string $paths, 97 | array|Collection $data = [] 98 | ): void; 99 | 100 | /** 101 | * Loops through an array of items and includes a subview for each. Use 102 | * the `$var` variable to set a variable name for the item when passed 103 | * to the subview. Pass a fallback view name via `$empty` to show if 104 | * the items array is empty. 105 | * 106 | * @since 1.0.0 107 | */ 108 | public function each( 109 | array|string $paths, 110 | iterable $items = [], 111 | string $var = '', 112 | array|string $empty = [] 113 | ): void; 114 | 115 | /** 116 | * Returns a template tag object or null when it doesn't exist. 117 | * 118 | * @since 1.0.0 119 | */ 120 | public function tag( string $name, mixed ...$args ): ?TemplateTag; 121 | } 122 | -------------------------------------------------------------------------------- /src/Contracts/Template/TemplateTag.php: -------------------------------------------------------------------------------- 1 | tagName()`). Developers can create custom constructors with any 8 | * number of parameters, required or not, and the engine will pass down those 9 | * that are input. Essentially, this is just a way of extending the template 10 | * engine for custom use cases. 11 | * 12 | * @package Blush 13 | * @author Justin Tadlock 14 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 15 | * @link https://github.com/blush-dev/framework 16 | * @license https://opensource.org/licenses/MIT 17 | */ 18 | 19 | namespace Blush\Contracts\Template; 20 | 21 | use Stringable; 22 | use Blush\Contracts\{CastsToHtml, CastsToText}; 23 | use Blush\Tools\Collection; 24 | 25 | interface TemplateTag extends CastsToHtml, CastsToText, Stringable 26 | { 27 | /** 28 | * Sets the data for the tag. 29 | * 30 | * @since 1.0.0 31 | */ 32 | public function setData( Collection $data ): void; 33 | } 34 | -------------------------------------------------------------------------------- /src/Contracts/Template/TemplateTags.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Template; 13 | 14 | // Concretes. 15 | use Blush\Tools\Collection; 16 | 17 | interface TemplateTags 18 | { 19 | /** 20 | * Creates a new tag object if it exists. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public function callback( 25 | string $name, 26 | Collection $data, 27 | array $args = [] 28 | ): ?TemplateTag; 29 | } 30 | -------------------------------------------------------------------------------- /src/Contracts/Template/TemplateView.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Contracts\Template; 13 | 14 | // Concretes. 15 | use Blush\Tools\Collection; 16 | 17 | interface TemplateView 18 | { 19 | /** 20 | * Sets the view data. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public function setData( Collection $data ): void; 25 | 26 | /** 27 | * Gets the view data. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function getData(): Collection; 32 | 33 | /** 34 | * Returns the located template. 35 | * 36 | * @since 1.0.0 37 | */ 38 | public function template(): string; 39 | 40 | /** 41 | * Displays the view. 42 | * 43 | * @since 1.0.0 44 | */ 45 | public function display(): void; 46 | 47 | /** 48 | * Returns the view. 49 | * 50 | * @since 1.0.0 51 | */ 52 | public function render(): string; 53 | } 54 | -------------------------------------------------------------------------------- /src/Controllers/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Config}; 15 | use Blush\Core\Proxies\Cache as CacheRegistry; 16 | use Blush\Content\Entry\Virtual; 17 | use Blush\Template\Tag\DocumentTitle; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class Cache extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | $purge_key = Config::get( 'cache.purge_key' ); 30 | 31 | $store = $params['name'] ?? ''; 32 | $key = $params['key'] ?? ''; 33 | $flushed = false; 34 | 35 | $title = 'Cache Flush Failure'; 36 | $content = '

Invalid cache flush request.

'; 37 | 38 | // Flush cache store. 39 | if ( $store && $key && $key === $purge_key ) { 40 | if ( $this->flushCacheStore( $store ) ) { 41 | $title = 'Cache Store Flushed'; 42 | $content = sprintf( 43 | '

Successfully flushed and purged all data from the %s cache store.

', 44 | CacheRegistry::store( $store )->store() 45 | ); 46 | } 47 | } 48 | 49 | // Flush all stores. 50 | if ( ! $store && ! $flushed && $key && $key === $purge_key ) { 51 | if ( $this->flushAllCacheStores() ) { 52 | $title = 'Cache Stores Flushed'; 53 | $content = sprintf( '

Successfully flushed and purged data from all cache stores.

' ); 54 | } 55 | } 56 | 57 | // Create a virtual entry for the content. 58 | $single = new Virtual( [ 59 | 'content' => $content, 60 | 'meta' => [ 'title' => $title ] 61 | ] ); 62 | 63 | $doctitle = new DocumentTitle( $single->title() ); 64 | 65 | return $this->response( $this->view( [ 66 | "single-page-cache", 67 | 'single-page', 68 | 'single', 69 | 'index' 70 | ], [ 71 | 'doctitle' => $doctitle, 72 | 'pagination' => false, 73 | 'single' => $single, 74 | 'collection' => false 75 | ] ) ); 76 | } 77 | 78 | /** 79 | * Flushes a cache store. 80 | * 81 | * @since 1.0.0 82 | */ 83 | private function flushCacheStore( string $name ): bool 84 | { 85 | if ( CacheRegistry::storeExists( $name ) ) { 86 | CacheRegistry::store( $name )->flush(); 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | /** 94 | * Flushes all cache stores. 95 | * 96 | * @since 1.0.0 97 | */ 98 | private function flushAllCacheStores(): bool 99 | { 100 | CacheRegistry::purge(); 101 | return true; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Controllers/Collection.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Query}; 15 | use Blush\Template\Hierarchy; 16 | use Blush\Template\Tag\{DocumentTitle, Pagination}; 17 | use Blush\Tools\Str; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class Collection extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | // Get all content types. 30 | $types = App::get( 'content.types' ); 31 | 32 | // Get needed URI params from the router. 33 | $path = $params['path']; 34 | $page = intval( $params['page'] ?? 1 ); 35 | 36 | // If this is a paged view, strip the page from the path. 37 | if ( Str::contains( $path, "/page/{$page}" ) ) { 38 | $path = Str::beforeFirst( $path, "/page/{$page}" ); 39 | } 40 | 41 | // Get the content type from the path or URI. 42 | $type = $types->getTypeFromPath( $path ) ?: $types->getTypeFromUri( $path ); 43 | 44 | // Bail if there is no type. 45 | if ( ! $type ) { 46 | return $this->forward404( $params, $request ); 47 | } 48 | 49 | // Get the collection type. 50 | $collect = $types->get( $type->collect() ); 51 | 52 | // Query the content type's index file. 53 | $single = Query::make( [ 54 | 'path' => $type->path(), 55 | 'slug' => 'index' 56 | ] )->single(); 57 | 58 | // Merge the default collection query args for the type 59 | // with user query args. 60 | $query_args = array_merge( 61 | $type->collectionArgs(), 62 | $single ? $single->collectionArgs() : [] 63 | ); 64 | 65 | // Set required variables for the query. 66 | $query_args['number'] = $query_args['number'] ?? 10; 67 | $query_args['offset'] = $query_args['number'] * ( $page - 1 ); 68 | 69 | // Query the content type collection. 70 | $collection = Query::make( $query_args ); 71 | 72 | if ( $single && $collection->hasEntries() ) { 73 | 74 | $type_name = sanitize_slug( $type->type() ); 75 | $model_name = $type->isTaxonomy() ? 'taxonomy' : 'content'; 76 | 77 | $doctitle = new DocumentTitle( $single->title(), [ 78 | 'page' => $page 79 | ] ); 80 | 81 | $pagination = new Pagination( [ 82 | 'basepath' => $path, 83 | 'current' => $page, 84 | 'total' => $collection->pages() 85 | ] ); 86 | 87 | return $this->response( $this->view( 88 | Hierarchy::collection( $single ), 89 | [ 90 | 'doctitle' => $doctitle, 91 | 'pagination' => $pagination, 92 | 'single' => $single, 93 | 'collection' => $collection 94 | ] 95 | ) ); 96 | } 97 | 98 | // If all else fails, return a 404. 99 | return $this->forward404( $params, $request ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Controllers/CollectionArchiveDate.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Query}; 15 | use Blush\Content\Entry\Virtual; 16 | use Blush\Template\Hierarchy; 17 | use Blush\Template\Tag\{DocumentTitle, Pagination}; 18 | use Blush\Tools\Str; 19 | use Symfony\Component\HttpFoundation\{Request, Response}; 20 | 21 | class CollectionArchiveDate extends Controller 22 | { 23 | /** 24 | * Callback method when route matches request. 25 | * 26 | * @since 1.0.0 27 | */ 28 | public function __invoke( array $params, Request $request ): Response 29 | { 30 | $types = App::resolve( 'content.types' ); 31 | 32 | // Get or set params. 33 | $path = $basepath = $params['path'] ?? ''; 34 | $page = intval( $params['page'] ?? 1 ); 35 | $second = $params['second'] ?? ''; 36 | $minute = $params['minute'] ?? ''; 37 | $hour = $params['hour'] ?? ''; 38 | $day = $params['day'] ?? ''; 39 | $month = $params['month'] ?? ''; 40 | $year = $params['year'] ?? ''; 41 | $type = false; 42 | 43 | // Strip page from path. 44 | if ( Str::contains( $path, "/page/{$page}" ) ) { 45 | $path = $basepath = Str::beforeFirst( $path, "/page/{$page}" ); 46 | } 47 | 48 | // Explodes the path into parts and loops through each. Strips 49 | // the last part off the original path with each iteration and 50 | // checks if it's the path or URI for the content type. If a 51 | // match is found, break out of the loop. 52 | foreach ( explode( '/', $path ) as $part ) { 53 | $path = Str::beforeLast( $path, "/{$part}" ); 54 | 55 | // Check type by path and URI. 56 | if ( $type = $types->getTypeFromPath( $path ) ) { 57 | break; 58 | } elseif ( $type = $types->getTypeFromUri( $path ) ) { 59 | break; 60 | } 61 | } 62 | 63 | // If there is no type, bail early. 64 | if ( ! $type ) { 65 | return $this->forward404( $params, $request ); 66 | } 67 | 68 | // Get the content type collection vars. 69 | $query_args = $type->collectionArgs(); 70 | 71 | // Set required variables for the query. 72 | $query_args['number'] = $query_args['number'] ?? 10; 73 | $query_args['offset'] = $query_args['number'] * ( $page - 1 ); 74 | 75 | if ( $second ) { $query_args['second'] = $second; } 76 | if ( $minute ) { $query_args['minute'] = $minute; } 77 | if ( $hour ) { $query_args['hour'] = $hour; } 78 | if ( $day ) { $query_args['day'] = $day; } 79 | if ( $month ) { $query_args['month'] = $month; } 80 | if ( $year ) { $query_args['year'] = $year; } 81 | 82 | // Build the title for the type of date archive. 83 | if ( $second && $minute && $hour && $day && $month && $year ) { 84 | $title = date( 'F j, Y \@ H:i:s', strtotime( "{$year}-{$month}-{$day} {$hour}:{$minute}:{$second}" ) ); 85 | } elseif ( $minute && $hour && $day && $month && $year ) { 86 | $title = date( 'F j, Y \@ H:i', strtotime( "{$year}-{$month}-{$day} {$hour}:{$minute}:00" ) ); 87 | } elseif ( $hour && $day && $month && $year ) { 88 | $title = date( 'F j, Y \@ H', strtotime( "{$year}-{$month}-{$day} {$hour}:00:00" ) ); 89 | } elseif ( $day && $month && $year ) { 90 | $title = date( 'F j, Y', strtotime( "{$year}-{$month}-{$day}" ) ); 91 | } elseif ( $month && $year ) { 92 | $title = date( 'F Y', strtotime( "{$year}-{$month}" ) ); 93 | } elseif ( $year ) { 94 | $title = date( 'Y', strtotime( $year ) ); 95 | } 96 | 97 | // Create a virtual entry for the archive data. 98 | $single = new Virtual( [ 99 | 'content' => '', 100 | 'meta' => [ 'title' => $title ?? 'Archives' ] 101 | ] ); 102 | 103 | // Query the content type collection. 104 | $collection = Query::make( $query_args ); 105 | 106 | if ( $collection->all() ) { 107 | $type_name = $type->name(); 108 | 109 | $doctitle = new DocumentTitle( $single->title(), [ 110 | 'page' => $page 111 | ] ); 112 | 113 | $pagination = new Pagination( [ 114 | 'basepath' => $basepath, 115 | 'current' => $page, 116 | 'total' => $collection->pages() 117 | ] ); 118 | 119 | return $this->response( $this->view( 120 | Hierarchy::collectionDate( $type ), 121 | [ 122 | 'doctitle' => $doctitle, 123 | 'pagination' => $pagination, 124 | 'single' => $single, 125 | 'collection' => $collection 126 | ] 127 | ) ); 128 | } 129 | 130 | // If all else fails, return a 404. 131 | return $this->forward404( $params, $request ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Controllers/CollectionFeed.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Config, Query}; 15 | use Blush\Feed\Writer; 16 | use Blush\Template\Tag\{DocumentTitle, Pagination}; 17 | use Blush\Tools\{Collection, Str}; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class CollectionFeed extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | // Get all content types. 30 | $types = App::get( 'content.types' ); 31 | $type = false; 32 | 33 | // Get needed URI params from the router. 34 | $path = Str::trimSlashes( Str::beforeLast( $params['path'], 'feed' ) ); 35 | 36 | // If there is no path, we're looking at the homepage feed. 37 | // Get the alias type if there is one. 38 | if ( ! $path && $alias = Config::get( 'app.home_alias' ) ) { 39 | $type = $types->has( $alias ) ? $types->get( $alias ) : false; 40 | } 41 | 42 | // Get the content type from the path or URI. 43 | if ( ! $type && $path ) { 44 | $type = $types->getTypeFromPath( $path ) ?: $types->getTypeFromUri( $path ); 45 | } 46 | 47 | // Bail if there is no type. 48 | if ( ! $type ) { 49 | return $this->forward404( $params, $request ); 50 | } 51 | 52 | // Query the content type's index file. 53 | $single = Query::make( [ 54 | 'path' => $type->path(), 55 | 'slug' => 'index' 56 | ] )->single(); 57 | 58 | // Query the content type collection. 59 | $collection = Query::make( $type->feedArgs() ); 60 | 61 | if ( $single && $collection->hasEntries() ) { 62 | 63 | $type_name = sanitize_slug( $type->type() ); 64 | $model_name = $type->isTaxonomy() ? 'taxonomy' : 'content'; 65 | 66 | // Get the feed view. 67 | return $this->response( $this->view( 68 | [ 69 | "feed-{$type_name}", 70 | "feed-{$model_name}", 71 | 'feed-rss', 72 | 'feed' 73 | ], [ 74 | 'doctitle' => new DocumentTitle(), 75 | 'pagination' => false, 76 | 'single' => $single, 77 | 'collection' => $collection, 78 | 'feed' => new Writer( $single, $collection, 'rss2' ) 79 | ] 80 | ), Response::HTTP_OK, [ 'content-type' => 'text/xml' ] ); 81 | } 82 | 83 | // If all else fails, return a 404. 84 | return $this->forward404( $params, $request ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Controllers/CollectionFeedAtom.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Config, Query}; 15 | use Blush\Feed\Writer; 16 | use Blush\Template\Tag\{DocumentTitle, Pagination}; 17 | use Blush\Tools\{Collection, Str}; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class CollectionFeedAtom extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | // Get all content types. 30 | $types = App::get( 'content.types' ); 31 | $type = false; 32 | 33 | // Get needed URI params from the router. 34 | $path = Str::trimSlashes( Str::beforeLast( $params['path'], 'feed' ) ); 35 | 36 | // If there is no path, we're looking at the homepage feed. 37 | // Get the alias type if there is one. 38 | if ( ! $path && $alias = Config::get( 'app.home_alias' ) ) { 39 | $type = $types->has( $alias ) ? $types->get( $alias ) : false; 40 | } 41 | 42 | // Get the content type from the path or URI. 43 | if ( ! $type && $path ) { 44 | $type = $types->getTypeFromPath( $path ) ?: $types->getTypeFromUri( $path ); 45 | } 46 | 47 | // Bail if there is no type. 48 | if ( ! $type ) { 49 | return $this->forward404( $params, $request ); 50 | } 51 | 52 | // Query the content type's index file. 53 | $single = Query::make( [ 54 | 'path' => $type->path(), 55 | 'slug' => 'index' 56 | ] )->single(); 57 | 58 | // Query the content type collection. 59 | $collection = Query::make( $type->feedArgs() ); 60 | 61 | if ( $single && $collection->hasEntries() ) { 62 | 63 | $type_name = sanitize_slug( $type->type() ); 64 | $model_name = $type->isTaxonomy() ? 'taxonomy' : 'content'; 65 | 66 | // Get the feed view. 67 | return $this->response( $this->view( 68 | [ 69 | "feed-{$type_name}", 70 | "feed-{$model_name}", 71 | 'feed-atom' 72 | ], [ 73 | 'doctitle' => new DocumentTitle(), 74 | 'pagination' => false, 75 | 'single' => $single, 76 | 'collection' => $collection, 77 | 'feed' => new Writer( $single, $collection, 'atom' ) 78 | ] 79 | ), Response::HTTP_OK, [ 'content-type' => 'text/xml' ] ); 80 | } 81 | 82 | // If all else fails, return a 404. 83 | return $this->forward404( $params, $request ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Controllers/CollectionTaxonomyTerm.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Query}; 15 | use Blush\Template\Hierarchy; 16 | use Blush\Template\Tag\{DocumentTitle, Pagination}; 17 | use Blush\Tools\Str; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class CollectionTaxonomyTerm extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | $types = App::get( 'content.types' ); 30 | 31 | $path = $params['path']; 32 | $name = $params['name']; 33 | $page = intval( $params['page'] ?? 1 ); 34 | 35 | $type_path = Str::beforeLast( $path, "/{$name}" ); 36 | 37 | if ( Str::contains( $path, "/page/{$page}" ) ) { 38 | $path = Str::beforeFirst( $path, "/page/{$page}" ); 39 | } 40 | 41 | // Get the taxonomy's content type. 42 | $type = $types->getTypeFromPath( $type_path ); 43 | $collect = $types->get( $type->termCollect() ); 44 | 45 | // Query the taxonomy term. 46 | $single = Query::make( [ 47 | 'path' => $type->path(), 48 | 'slug' => $name 49 | ] )->single(); 50 | 51 | // Merge the default collection query args for the type 52 | // with user query args. 53 | $query_args = array_merge( 54 | $type->termCollectionArgs(), 55 | $single ? $single->collectionArgs() : [] 56 | ); 57 | 58 | // Set required variables for the query. 59 | $query_args['number'] = $query_args['number'] ?? 10; 60 | $query_args['offset'] = $query_args['number'] * ( $page - 1 ); 61 | 62 | // Query the term's content collection. 63 | $collection = Query::make( array_merge( $query_args, [ 64 | 'meta_key' => $type->type(), 65 | 'meta_value' => $name 66 | ] ) ); 67 | 68 | if ( $single && $single->isPublic() && $collection->all() ) { 69 | $type_name = sanitize_slug( $type->type() ); 70 | 71 | $doctitle = new DocumentTitle( $single->title(), [ 72 | 'page' => $page 73 | ] ); 74 | 75 | $pagination = new Pagination( [ 76 | 'basepath' => $path, 77 | 'current' => $page, 78 | 'total' => $collection->pages() 79 | ] ); 80 | 81 | return $this->response( $this->view( 82 | Hierarchy::collectionTerm( $single ), 83 | [ 84 | 'doctitle' => $doctitle, 85 | 'pagination' => $pagination, 86 | 'single' => $single, 87 | 'collection' => $collection 88 | ] 89 | ) ); 90 | } 91 | 92 | // If all else fails, return a 404. 93 | return $this->forward404( $params, $request ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 13 | * @link https://github.com/blush-dev/framework 14 | * @license https://opensource.org/licenses/MIT 15 | */ 16 | 17 | namespace Blush\Controllers; 18 | 19 | // Abstracts. 20 | use Blush\Contracts\Template\TemplateView; 21 | 22 | // Concretes. 23 | use Blush\Core\Proxies\Engine; 24 | use Symfony\Component\HttpFoundation\{Request, Response}; 25 | 26 | abstract class Controller 27 | { 28 | /** 29 | * Callback method when route matches request. 30 | * 31 | * @since 1.0.0 32 | */ 33 | public function __invoke( array $params, Request $request ): Response 34 | { 35 | return $this->response( $this->view( 'index' ) ); 36 | } 37 | 38 | /** 39 | * Wrapper for the template engine view class. 40 | * 41 | * @since 1.0.0 42 | */ 43 | protected function view( string|array $names, Collection|array $data = [] ): TemplateView 44 | { 45 | return Engine::first( (array) $names, $data ); 46 | } 47 | 48 | /** 49 | * Wrapper for sending a new response to the browser. 50 | * 51 | * @since 1.0.0 52 | */ 53 | protected function response( TemplateView $view, int $status = 200, array $headers = [] ): Response 54 | { 55 | return new Response( $view->render(), $status, $headers ); 56 | } 57 | 58 | /** 59 | * Forwards request to another controller. 60 | * 61 | * @since 1.0.0 62 | */ 63 | protected function forward( string $callback, array $params, Request $request ): Response 64 | { 65 | $controller = new $callback; 66 | return $controller( $params, $request ); 67 | } 68 | 69 | /** 70 | * Forwards request to the `Error404` controller. 71 | * 72 | * @since 1.0.0 73 | */ 74 | protected function forward404( array $params, Request $request ): Response 75 | { 76 | return $this->forward( Error404::class, $params, $request ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Controllers/Error404.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Query}; 15 | use Blush\Content\Entry\Virtual; 16 | use Blush\Template\Hierarchy; 17 | use Blush\Template\Tag\DocumentTitle; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class Error404 extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | $single = Query::make( [ 30 | 'path' => '_error', 31 | 'slug' => '404' 32 | ] )->single(); 33 | 34 | // Create a virtual entry if no user-provided entry. 35 | if ( ! $single ) { 36 | $single = new Virtual( [ 37 | 'content' => '

Sorry, nothing was found here.

', 38 | 'meta' => [ 'title' => 'Nothing Found' ] 39 | ] ); 40 | } 41 | 42 | return $this->response( $this->view( 43 | Hierarchy::error404(), 44 | [ 45 | 'doctitle' => new DocumentTitle( $single->title() ), 46 | 'pagination' => false, 47 | 'single' => $single, 48 | 'collection' => false 49 | ] 50 | ), Response::HTTP_NOT_FOUND ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Controllers/Home.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Config, Message, Query}; 15 | use Blush\Template\Hierarchy; 16 | use Blush\Template\Tag\{DocumentTitle, Pagination}; 17 | use Blush\Tools\Str; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class Home extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | $types = App::resolve( 'content.types' ); 30 | $alias = Config::get( 'app.home_alias' ); 31 | $type = false; 32 | 33 | // Checks if the homepage has an alias content type and if the 34 | // type exists. 35 | if ( $alias && $types->has( $alias ) ) { 36 | $type = $types->get( $alias ); 37 | $collect = $types->get( $type->collect() ); 38 | } 39 | 40 | // If we have a content type and a collection type, run query. 41 | if ( $type && $collect ) { 42 | $page = intval( $params['page'] ?? 1 ); 43 | 44 | // Query the content type. 45 | $single = Query::make( [ 46 | 'path' => $type->path(), 47 | 'slug' => 'index' 48 | ] )->single(); 49 | 50 | // Merge the default collection query args for the type 51 | // with user query args. 52 | $query_args = array_merge( 53 | $type->collectionArgs(), 54 | $single ? $single->collectionArgs() : [] 55 | ); 56 | 57 | // Set required variables for the query. 58 | $query_args['number'] = $query_args['number'] ?? 10; 59 | $query_args['offset'] = $query_args['number'] * ( $page - 1 ); 60 | 61 | // Query the content type collection. 62 | $collection = Query::make( $query_args ); 63 | 64 | if ( $single && $collection->all() ) { 65 | $type_name = sanitize_slug( $type->type() ); 66 | 67 | $doctitle = new DocumentTitle( '', [ 68 | 'page' => $page 69 | ] ); 70 | 71 | $pagination = new Pagination( [ 72 | 'basepath' => '', 73 | 'current' => $page, 74 | 'total' => $collection->pages() 75 | ] ); 76 | 77 | return $this->response( $this->view( 78 | Hierarchy::collectionHome( $single ), 79 | [ 80 | 'doctitle' => $doctitle, 81 | 'pagination' => $pagination, 82 | 'single' => $single, 83 | 'collection' => $collection 84 | ] 85 | ) ); 86 | } 87 | } 88 | 89 | // Query the homepage `index.md` file. 90 | $single = Query::make( [ 'slug' => 'index' ] )->single(); 91 | 92 | if ( $single && $single->isPublic() ) { 93 | $collection = false; 94 | 95 | if ( $args = $single->collectionArgs() ) { 96 | $collection = Query::make( $args ); 97 | } 98 | 99 | return $this->response( $this->view( 100 | Hierarchy::singleHome( $single ), 101 | [ 102 | 'doctitle' => new DocumentTitle(), 103 | 'pagination' => false, 104 | 'single' => $single, 105 | 'collection' => $collection 106 | ] 107 | ) ); 108 | } 109 | 110 | // If no index file is found, which is the minimum necessary for 111 | // a site, we'll dump a notice and return an empty response. 112 | // Note that this is not a 404. It is a user error. 113 | $notice = sprintf( 114 | 'No %s file found.', 115 | Str::appendPath( App::get( 'path.content' ), 'index.md' ) 116 | ); 117 | 118 | Message::make( $notice )->dump(); 119 | 120 | return new Response( '' ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Controllers/Single.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Query}; 15 | use Blush\Template\Hierarchy; 16 | use Blush\Template\Tag\DocumentTitle; 17 | use Blush\Tools\Str; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class Single extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | $types = App::resolve( 'content.types' ); 30 | 31 | // Check if another content type is part of the request. In 32 | // particular, this is mostly used for taxonomies used in the URL. 33 | foreach ( $types as $type ) { 34 | if ( isset( $params[ $type->name() ] ) ) { 35 | $meta_key = $type->name(); 36 | $meta_value = $params[ $type->name() ]; 37 | } 38 | } 39 | 40 | // Get the post name and path. 41 | $name = $params['name']; 42 | $path = $params['path'] ?? ''; 43 | $parts = explode( '/', $path ); 44 | 45 | // Explodes the path into parts and loops through each. Strips 46 | // the last part off the original path with each iteration and 47 | // checks if it's the path or URI for the content type. If a 48 | // match is found, break out of the loop. 49 | foreach ( array_reverse( $parts ) as $part ) { 50 | $path = Str::beforeLast( $path, "/{$part}" ); 51 | 52 | // Check type by path and URI. 53 | if ( $type = $types->getTypeFromPath( $path ) ) { 54 | break; 55 | } elseif ( $type = $types->getTypeFromUri( $path ) ) { 56 | break; 57 | } 58 | } 59 | 60 | $single = Query::make( [ 61 | 'path' => $type ? $type->path() : $path, 62 | 'slug' => $name, 63 | 'year' => $params['year'] ?? null, 64 | 'month' => $params['month'] ?? null, 65 | 'day' => $params['day'] ?? null, 66 | 'author' => $params['author'] ?? null, 67 | 'meta_key' => $meta_key ?? null, 68 | 'meta_value' => $meta_value ?? null 69 | ] )->single(); 70 | 71 | if ( $single && $single->isPublic() ) { 72 | $type_name = sanitize_slug( $type->type() ); 73 | $collection = false; 74 | 75 | if ( $args = $single->collectionArgs() ) { 76 | $collection = Query::make( $args ); 77 | } 78 | 79 | $doctitle = new DocumentTitle( $single->title() ); 80 | 81 | return $this->response( $this->view( 82 | Hierarchy::single( $single ), 83 | [ 84 | 'doctitle' => $doctitle, 85 | 'pagination' => false, 86 | 'single' => $single, 87 | 'collection' => $collection 88 | ] 89 | ) ); 90 | } 91 | 92 | // If all else fails, return a 404. 93 | return $this->forward404( $params, $request ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Controllers/SinglePage.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Query}; 15 | use Blush\Template\Hierarchy; 16 | use Blush\Template\Tag\DocumentTitle; 17 | use Blush\Tools\Str; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class SinglePage extends Single 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | $path = $params['path'] ?? ''; 30 | $name = Str::afterLast( $path, '/' ); 31 | 32 | // If the page name begins with `_`, it is private. 33 | if ( Str::startsWith( $name, '_' ) ) { 34 | return $this->forward404( $params, $request ); 35 | } 36 | 37 | // Look for an `path/index.md` file. 38 | $single = Query::make( [ 39 | 'path' => $path, 40 | 'slug' => 'index' 41 | ] )->single(); 42 | 43 | // Look for a `path/{$name}.md` file if `path/index.md` not found. 44 | if ( ! $single ) { 45 | $single = Query::make( [ 46 | 'path' => Str::beforeLast( $path, '/' ), 47 | 'slug' => $name 48 | ] )->single(); 49 | } 50 | 51 | if ( $single && $single->isPublic() ) { 52 | $collection = false; 53 | 54 | if ( $args = $single->collectionArgs() ) { 55 | $collection = Query::make( $args ); 56 | } 57 | 58 | $doctitle = new DocumentTitle( $single->title() ); 59 | 60 | return $this->response( $this->view( 61 | Hierarchy::single( $single ), 62 | [ 63 | 'doctitle' => $doctitle, 64 | 'pagination' => false, 65 | 'single' => $single, 66 | 'collection' => $collection 67 | ] 68 | ) ); 69 | } 70 | 71 | // If all else fails, return a 404. 72 | return $this->forward404( $params, $request ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Controllers/Sitemap.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Config, Query}; 15 | use Blush\Content\Entry\Virtual; 16 | use Blush\Template\Tag\{DocumentTitle}; 17 | use Blush\Tools\{Collection, Str}; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class Sitemap extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | // Get all content types. 30 | $types = App::get( 'content.types' ); 31 | 32 | $type = $params['type'] ?? ''; 33 | 34 | if ( ! $type ) { 35 | return $this->forward404( $params, $request ); 36 | } 37 | 38 | // Query the sitemap's index file. 39 | $single = new Virtual( [ 40 | 'content' => '', 41 | 'meta' => [ 'title' => 'Sitemap' ] 42 | ] ); 43 | 44 | // @todo collect content from all post types. 45 | $collection = Query::make( [ 46 | 'type' => $types->get( $type )->name(), 47 | 'number' => 0, 48 | 'orderby' => 'published', 49 | 'order' => 'desc', 50 | 'nocontent' => true 51 | ] ); 52 | 53 | if ( $single && $collection->hasEntries() ) { 54 | 55 | // Get the feed view. 56 | return $this->response( $this->view( 57 | [ 58 | 'sitemap' 59 | ], [ 60 | 'doctitle' => new DocumentTitle(), 61 | 'pagination' => false, 62 | 'single' => $single, 63 | 'collection' => $collection, 64 | ] 65 | ), Response::HTTP_OK, [ 'content-type' => 'text/xml' ] ); 66 | } 67 | 68 | // If all else fails, return a 404. 69 | return $this->forward404( $params, $request ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Controllers/SitemapIndex.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Controllers; 13 | 14 | use Blush\Core\Proxies\{App, Config, Query}; 15 | use Blush\Content\Entry\Virtual; 16 | use Blush\Template\Tag\{DocumentTitle}; 17 | use Blush\Tools\{Collection, Str}; 18 | use Symfony\Component\HttpFoundation\{Request, Response}; 19 | 20 | class SitemapIndex extends Controller 21 | { 22 | /** 23 | * Callback method when route matches request. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __invoke( array $params, Request $request ): Response 28 | { 29 | // Get all content types. 30 | $types = App::get( 'content.types' ); 31 | 32 | // Query the sitemap's index file. 33 | $single = Query::make( [ 34 | 'path' => 'sitemap', 35 | 'slug' => 'index' 36 | ] )->single(); 37 | 38 | // Create a virtual entry if no user-provided entry. 39 | if ( ! $single ) { 40 | $single = new Virtual( [ 41 | 'content' => '', 42 | 'meta' => [ 'title' => 'Sitemap Index' ] 43 | ] ); 44 | } 45 | 46 | $sitemaps = new Collection(); 47 | 48 | foreach ( $types as $type ) { 49 | $sitemaps->add( $type->name(), new class( $type ) { 50 | public function __construct( protected $type ) {} 51 | public function url(): string 52 | { 53 | return url( 'sitemap/' . $this->type->name() ); 54 | } 55 | } ); 56 | } 57 | 58 | if ( $single ) { 59 | 60 | // Get the feed view. 61 | return $this->response( $this->view( 62 | [ 63 | 'sitemap-index' 64 | ], [ 65 | 'doctitle' => new DocumentTitle(), 66 | 'pagination' => false, 67 | 'single' => $single, 68 | 'collection' => null, 69 | 'sitemaps' => $types 70 | ] 71 | ), Response::HTTP_OK, [ 'content-type' => 'text/xml' ] ); 72 | } 73 | 74 | // If all else fails, return a 404. 75 | return $this->forward404( $params, $request ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Core/Providers/App.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Providers; 13 | 14 | // Abstracts. 15 | use Blush\Core\ServiceProvider; 16 | 17 | // Concretes. 18 | use Blush\Messenger\Message; 19 | use Symfony\Component\VarDumper\VarDumper; 20 | use Symfony\Component\VarDumper\Cloner\VarCloner; 21 | use Symfony\Component\VarDumper\Dumper\{HtmlDumper, CliDumper}; 22 | 23 | class App extends ServiceProvider 24 | { 25 | /** 26 | * Register bindings. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function register(): void 31 | { 32 | // Sets the default timezone. 33 | date_default_timezone_set( $this->app['config']->get( 'app.timezone' ) ); 34 | 35 | // Add messenger. 36 | $this->app->bind( Message::class ); 37 | 38 | // Add aliases. 39 | $this->app->alias( Message::class, 'messenger.message' ); 40 | 41 | // Set up variable dumper. 42 | $this->setVarDumper(); 43 | } 44 | 45 | /** 46 | * Sets the handler for Symfony's variable dumper. We are just making it 47 | * look a little prettier with custom styles. 48 | * 49 | * @since 1.0.0 50 | */ 51 | private function setVarDumper(): void 52 | { 53 | VarDumper::setHandler( function( $var ) { 54 | $cloner = new VarCloner(); 55 | $html_dumper = new HtmlDumper(); 56 | 57 | $html_dumper->setTheme( 'light' ); 58 | 59 | $html_dumper->setStyles( [ 60 | 'default' => ' 61 | box-sizing: border-box; 62 | position: relative; 63 | z-index: 99999; 64 | overflow: auto !important; 65 | word-break: break-all; 66 | word-wrap: normal; 67 | white-space: revert; 68 | margin: 2rem; 69 | max-width: 100%; 70 | padding: 2rem; 71 | font-family: \"Source Code Pro\", Monaco, Consolas, \"Andale Mono WT\", \"Andale Mono\", \"Lucida Console\", \"Lucida Sans Typewriter\", \"DejaVu Sans Mono\", \"Bitstream Vera Sans Mono\", \"Liberation Mono\", \"Nimbus Mono L\", \"Courier New\", Courier, monospace; 72 | font-size: 18px; 73 | line-height: 1.75; 74 | color: #334155; 75 | background: #f8fafc; 76 | border: 1px solid #e2e8f0; 77 | border-bottom-color: #cbd5e1; 78 | border-radius: 0; 79 | box-shadow: none; 80 | ', 81 | 'index' => 'color: #60a5fa;', 82 | 'key' => 'color: #16a34a;', 83 | 'meta' => 'color: #9333ea;', 84 | 'note' => 'color: #1d4ed8;', 85 | 'num' => 'color: #60a5fa;', 86 | 'private' => 'color: #64748b;', 87 | 'protected' => 'color: #475569;', 88 | 'ref' => 'color: #3b82f6;', 89 | 'str' => 'color: #16a34a;', 90 | 'toggle' => 'padding: 0 0.5rem' 91 | ] ); 92 | 93 | $dumper = PHP_SAPI === 'cli' ? new CliDumper() : $html_dumper; 94 | 95 | $dumper->dump( $cloner->cloneVar( $var ) ); 96 | } ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Core/Providers/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Providers; 13 | 14 | use Blush\Contracts\Cache\Registry as RegistryContract; 15 | 16 | use Blush\Core\ServiceProvider; 17 | use Blush\Cache\{Component, Registry}; 18 | use Blush\Cache\Drivers\{File, JsonFile}; 19 | 20 | class Cache extends ServiceProvider 21 | { 22 | /** 23 | * Register bindings. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function register(): void 28 | { 29 | // Bind cache registry. 30 | $this->app->singleton( RegistryContract::class, Registry::class ); 31 | 32 | // Binds the cache component. 33 | $this->app->singleton( Component::class, function( $app ) { 34 | 35 | // Merge default and user-configured drivers. 36 | $drivers = array_merge( [ 37 | 'file' => File::class, 38 | 'file.cache' => File::class, 39 | 'file.json' => JsonFile::class 40 | ], $app->make( 'config' )->get( 'cache.drivers' ) ); 41 | 42 | // Merge default and user-configured stores. 43 | $stores = array_merge( [ 44 | 'content' => [ 45 | 'driver' => 'file.json', 46 | 'path' => $app->cachePath( 'content' ) 47 | ], 48 | 'global' => [ 49 | 'driver' => 'file.cache', 50 | 'path' => $app->cachePath( 'global' ) 51 | ] 52 | ], $app->make( 'config' )->get( 'cache.stores' ) ); 53 | 54 | // Creates the cache component. 55 | return new Component( 56 | $app->make( RegistryContract::class ), 57 | $drivers, 58 | $stores 59 | ); 60 | } ); 61 | 62 | // Add aliases. 63 | $this->app->alias( RegistryContract::class, 'cache.registry' ); 64 | } 65 | 66 | /** 67 | * Bootstrap bindings. 68 | * 69 | * @since 1.0.0 70 | */ 71 | public function boot(): void 72 | { 73 | $this->app->make( Component::class )->boot(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Core/Providers/Content.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Providers; 13 | 14 | // Interfaces. 15 | use Blush\Contracts\Content\{ 16 | ContentEntry, 17 | ContentLocator, 18 | ContentQuery, 19 | ContentType, 20 | ContentTypes 21 | }; 22 | 23 | // Classes. 24 | use Blush\Core\ServiceProvider; 25 | use Blush\Content\Locator\File as FileLocator; 26 | use Blush\Content\Query\File as FileQuery; 27 | use Blush\Content\Entry\MarkdownFile; 28 | use Blush\Content\Type\{Component, Type, Types}; 29 | 30 | class Content extends ServiceProvider 31 | { 32 | /** 33 | * Register bindings. 34 | * 35 | * @since 1.0.0 36 | */ 37 | public function register(): void 38 | { 39 | // Bind content entry. 40 | $this->app->bind( ContentEntry::class, MarkdownFile::class ); 41 | 42 | // Bind content type. 43 | $this->app->bind( ContentType::class, Type::class ); 44 | 45 | // Bind content type registry singleton. 46 | $this->app->singleton( ContentTypes::class, Types::class ); 47 | 48 | // Bind content types component and pass types and config in. 49 | $this->app->singleton( Component::class, function( $app ) { 50 | 51 | // Merge default and user-configured content types. 52 | $types = array_merge( $app->make( 'config' )->get( 'content' ), [ 53 | 'page' => [ 54 | 'path' => '', 55 | 'routing' => false, 56 | 'collect' => false 57 | ] 58 | ] ); 59 | 60 | // Creates the content types component. 61 | return new Component( $app->make( ContentTypes::class ), $types ); 62 | } ); 63 | 64 | // Bind the content locator. 65 | $this->app->bind( ContentLocator::class, FileLocator::class ); 66 | 67 | // Bind the content query. 68 | $this->app->bind( ContentQuery::class, function( $app ) { 69 | return new FileQuery( $app->make( ContentLocator::class ) ); 70 | } ); 71 | 72 | // Add aliases. 73 | $this->app->alias( ContentEntry::class, 'content.entry' ); 74 | $this->app->alias( ContentLocator::class, 'content.locator' ); 75 | $this->app->alias( ContentQuery::class, 'content.query' ); 76 | $this->app->alias( ContentType::class, 'content.type' ); 77 | $this->app->alias( ContentTypes::class, 'content.types' ); 78 | } 79 | 80 | /** 81 | * Bootstrap bindings. 82 | * 83 | * @since 1.0.0 84 | */ 85 | public function boot(): void 86 | { 87 | $this->app->make( Component::class )->boot(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Core/Providers/Markdown.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Providers; 13 | 14 | use Blush\Contracts\Markdown\Parser as ParserContract; 15 | 16 | use Blush\Core\ServiceProvider; 17 | use Blush\Markdown\{Parser, ImageRenderer, LinkRenderer, ParagraphRenderer}; 18 | 19 | use League\CommonMark\{ConverterInterface, MarkdownConverter}; 20 | use League\CommonMark\Environment\Environment; 21 | use League\CommonMark\Extension\CommonMark\Node\Inline\{Image, Link}; 22 | use League\CommonMark\Node\Block\Paragraph; 23 | 24 | class Markdown extends ServiceProvider 25 | { 26 | /** 27 | * Register bindings. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function register(): void 32 | { 33 | // Sets up the Markdown converter and environment. 34 | $this->app->singleton( ConverterInterface::class, function( $app ) { 35 | 36 | // Gets the user Markdown config. 37 | $markdown = $app->get( 'config' )->get( 'markdown' ); 38 | 39 | // Configure the Environment. 40 | $environment = new Environment( $markdown['config'] ); 41 | 42 | // Loops through user-added extensions and adds them. 43 | foreach ( $markdown['extensions'] as $extension ) { 44 | $environment->addExtension( new $extension() ); 45 | } 46 | 47 | // Loops through user-added inline parsers and adds them. 48 | foreach ( $markdown['inline_parsers'] as $parser ) { 49 | $environment->addInlineParser( new $parser() ); 50 | } 51 | 52 | // Add default renderers. 53 | $renderers = [ 54 | Image::class => ImageRenderer::class, 55 | Link::class => LinkRenderer::class, 56 | Paragraph::class => ParagraphRenderer::class 57 | ]; 58 | 59 | // Loops through renderers and adds them. 60 | foreach ( $renderers as $node => $renderer ) { 61 | $environment->addRenderer( $node, new $renderer() ); 62 | } 63 | 64 | // Return Markdown converter instance. 65 | return new MarkdownConverter( $environment ); 66 | } ); 67 | 68 | // Binds a Markdown wrapper class for accessing the converter. 69 | $this->app->bind( ParserContract::class, function( $app ) { 70 | return new Parser( $app->make( ConverterInterface::class ) ); 71 | } ); 72 | 73 | $this->app->alias( ConverterInterface::class, 'markdown.converter' ); 74 | $this->app->alias( ParserContract::class, 'markdown' ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Core/Providers/Routing.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Providers; 13 | 14 | use Blush\Contracts\Routing\{ 15 | RoutingRoute, 16 | RoutingRouter, 17 | RoutingRoutes, 18 | RoutingUrl 19 | }; 20 | 21 | use Blush\Core\ServiceProvider; 22 | use Blush\Routing\{Component, Router, Url}; 23 | use Blush\Routing\Route\{Route, Routes}; 24 | 25 | class Routing extends ServiceProvider 26 | { 27 | /** 28 | * Register bindings. 29 | * 30 | * @since 1.0.0 31 | */ 32 | public function register(): void 33 | { 34 | // Bind route. 35 | $this->app->bind( RoutingRoute::class, Route::class ); 36 | 37 | // Bind routes. 38 | $this->app->singleton( RoutingRoutes::class, Routes::class ); 39 | 40 | // Binds the router. 41 | $this->app->singleton( RoutingRouter::class, function( $app ) { 42 | return new Router( $app->make( RoutingRoutes::class ) ); 43 | } ); 44 | 45 | // Binds the routing URL instance. 46 | $this->app->singleton( RoutingUrl::class, function( $app ) { 47 | return new Url( $app->make( RoutingRoutes::class ) ); 48 | } ); 49 | 50 | // Binds the routing component. 51 | $this->app->singleton( Component::class, function( $app ) { 52 | return new Component( 53 | $app->make( RoutingRoutes::class ), 54 | $app->make( 'content.types' ) 55 | ); 56 | } ); 57 | 58 | // Add aliases. 59 | $this->app->alias( RoutingRoute::class, 'routing.route' ); 60 | $this->app->alias( RoutingRoutes::class, 'routing.routes' ); 61 | $this->app->alias( RoutingRouter::class, 'routing.router' ); 62 | $this->app->alias( RoutingUrl::class, 'routing.url' ); 63 | } 64 | 65 | /** 66 | * Bootstrap bindings. 67 | * 68 | * @since 1.0.0 69 | */ 70 | public function boot(): void 71 | { 72 | $this->app->make( Component::class )->boot(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Core/Providers/Template.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Providers; 13 | 14 | use Blush\Contracts\Template\{ 15 | TemplateEngine, 16 | TemplateTag, 17 | TemplateTags, 18 | TemplateView 19 | }; 20 | 21 | use Blush\Core\ServiceProvider; 22 | use Blush\Template\{Component, Engine, View}; 23 | use Blush\Template\Tag\{PoweredBy, Tag, Tags}; 24 | use Blush\Tools\Collection; 25 | 26 | class Template extends ServiceProvider 27 | { 28 | /** 29 | * Register bindings. 30 | * 31 | * @since 1.0.0 32 | */ 33 | public function register(): void 34 | { 35 | // Add template engine. 36 | $this->app->singleton( TemplateEngine::class, Engine::class ); 37 | $this->app->bind( TemplateView::class, View::class ); 38 | 39 | // Bind template tag. 40 | $this->app->bind( TemplateTag::class, Tag::class ); 41 | 42 | // Bind template tags. 43 | $this->app->singleton( TemplateTags::class, Tags::class ); 44 | 45 | // Binds the template component. 46 | $this->app->singleton( Component::class, function( $app ) { 47 | return new Component( 48 | $app->make( TemplateTags::class ), 49 | $app->make( 'config' )->get( 'template.tags' ) 50 | ); 51 | } ); 52 | 53 | // Add powered-by singleton. 54 | $this->app->singleton( PoweredBy::class ); 55 | 56 | // Add aliases. 57 | $this->app->alias( TemplateTag::class, 'template.tag' ); 58 | $this->app->alias( TemplateTags::class, 'template.tags' ); 59 | $this->app->alias( TemplateView::class, 'template.view' ); 60 | $this->app->alias( TemplateEngine::class, 'template.engine' ); 61 | $this->app->alias( PoweredBy::class, 'poweredby' ); 62 | } 63 | 64 | /** 65 | * Bootstrap bindings. 66 | * 67 | * @since 1.0.0 68 | */ 69 | public function boot(): void 70 | { 71 | $this->app->make( Component::class )->boot(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Core/Proxies/App.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 10 | * @link https://github.com/blush-dev/framework 11 | * @license https://opensource.org/licenses/MIT 12 | */ 13 | 14 | namespace Blush\Core\Proxies; 15 | 16 | use Blush\Core\Proxy; 17 | 18 | class App extends Proxy 19 | { 20 | /** 21 | * Returns the name of the accessor for object registered in the container. 22 | * 23 | * @since 1.0.0 24 | */ 25 | protected static function accessor(): string { 26 | return 'app'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Core/Proxies/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | use Blush\Cache\Registry; 16 | 17 | class Cache extends Proxy 18 | { 19 | /** 20 | * Returns the name of the accessor for object registered in the container. 21 | * 22 | * @since 1.0.0 23 | */ 24 | protected static function accessor(): string 25 | { 26 | return 'cache.registry'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Core/Proxies/Config.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | use Blush\Cache\Registry; 16 | 17 | class Config extends Proxy 18 | { 19 | /** 20 | * Returns the name of the accessor for object registered in the container. 21 | * 22 | * @since 1.0.0 23 | */ 24 | protected static function accessor(): string 25 | { 26 | return 'config'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Core/Proxies/Engine.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | 16 | class Engine extends Proxy 17 | { 18 | /** 19 | * Returns the name of the accessor for object registered in the container. 20 | * 21 | * @since 1.0.0 22 | */ 23 | protected static function accessor(): string 24 | { 25 | return 'template.engine'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Core/Proxies/Message.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | 16 | class Message extends Proxy 17 | { 18 | /** 19 | * Returns the name of the accessor for object registered in the container. 20 | * 21 | * @since 1.0.0 22 | */ 23 | protected static function accessor(): string { 24 | return 'messenger.message'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Proxies/PoweredBy.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | 16 | class PoweredBy extends Proxy 17 | { 18 | /** 19 | * Returns the name of the accessor for object registered in the container. 20 | * 21 | * @since 1.0.0 22 | */ 23 | protected static function accessor(): string { 24 | return 'poweredby'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Proxies/Query.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | use Blush\Contracts\Content\Query as QueryContract; 16 | 17 | class Query extends Proxy 18 | { 19 | /** 20 | * Returns the name of the accessor for object registered in the container. 21 | * 22 | * @since 1.0.0 23 | */ 24 | protected static function accessor(): string 25 | { 26 | return 'content.query'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Core/Proxies/Url.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Proxies; 13 | 14 | use Blush\Core\Proxy; 15 | 16 | class Url extends Proxy 17 | { 18 | /** 19 | * Returns the name of the accessor for object registered in the container. 20 | * 21 | * @since 1.0.0 22 | */ 23 | protected static function accessor(): string { 24 | return 'routing.url'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Proxy.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 11 | * @link https://github.com/blush-dev/framework 12 | * @license https://opensource.org/licenses/MIT 13 | */ 14 | 15 | namespace Blush\Core; 16 | 17 | use Blush\Contracts\Core\Container; 18 | 19 | class Proxy 20 | { 21 | /** 22 | * The container object. 23 | * 24 | * @since 1.0.0 25 | */ 26 | protected static Container $container; 27 | 28 | /** 29 | * Returns the name of the accessor for object registered in the container. 30 | * 31 | * @since 1.0.0 32 | */ 33 | protected static function accessor(): string 34 | { 35 | return ''; 36 | } 37 | 38 | /** 39 | * Sets the container object. 40 | * 41 | * @since 1.0.0 42 | */ 43 | public static function setContainer( Container $container ): void 44 | { 45 | static::$container = $container; 46 | } 47 | 48 | /** 49 | * Returns the instance from the container. 50 | * 51 | * @since 1.0.0 52 | */ 53 | protected static function instance(): object 54 | { 55 | return static::$container->resolve( static::accessor() ); 56 | } 57 | 58 | /** 59 | * Calls the requested method from the object registered with the 60 | * container statically. 61 | * 62 | * @since 1.0.0 63 | */ 64 | public static function __callStatic( string $method, array $args ): mixed 65 | { 66 | $instance = static::instance(); 67 | return $instance ? $instance->$method( ...$args ) : null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Core/Schemas/App.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Schemas; 13 | 14 | use Nette\Schema\Expect; 15 | use Nette\Schema\Schema; 16 | 17 | class App 18 | { 19 | /** 20 | * Returns the schema structure. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public static function schema(): Schema 25 | { 26 | return Expect::structure( [ 27 | 'url' => Expect::string( 'http://localhost' ), 28 | 'title' => Expect::string( 'Blush' ), 29 | 'tagline' => Expect::string( '' ), 30 | 'timezone' => Expect::string( 'America/Chicago' ), 31 | 'date_format' => Expect::string( 'F j, Y' ), 32 | 'time_format' => Expect::string( 'g:i a' ), 33 | 'home_alias' => Expect::string( '' ), 34 | 'sitemap' => Expect::bool( false ), 35 | 'providers' => Expect::array( [] ), 36 | 'proxies' => Expect::array( [] ), 37 | 38 | // @deprecated 1.0.0 Soft deprecation in favor of `url`. 39 | 'uri' => Expect::string( '' ) 40 | ] ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Core/Schemas/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Schemas; 13 | 14 | use Nette\Schema\Expect; 15 | use Nette\Schema\Schema; 16 | 17 | class Cache 18 | { 19 | /** 20 | * Returns the schema structure. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public static function schema(): Schema 25 | { 26 | return Expect::structure( [ 27 | 'purge_key' => Expect::string( '' ), 28 | 'expires' => Expect::int( 0 ), 29 | 'content_exclude_meta' => Expect::array( [] ), 30 | 'global' => Expect::bool( false ), 31 | 'global_exclude' => Expect::array( [] ), 32 | 'stores' => Expect::arrayOf( 'array', 'string' ), 33 | 'drivers' => Expect::arrayOf( 'string', 'string' ), 34 | 35 | // @todo - Remove. No longer in use. 36 | 'markdown' => Expect::bool( false ) 37 | ] ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Core/Schemas/Content.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Schemas; 13 | 14 | use Nette\Schema\Expect; 15 | use Nette\Schema\Schema; 16 | 17 | class Content 18 | { 19 | /** 20 | * Returns the schema structure. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public static function schema(): Schema 25 | { 26 | return Expect::arrayOf( Expect::structure( [ 27 | 'path' => Expect::string(), 28 | 'collect' => Expect::type( 'string|bool' )->nullable(), 29 | 'collection' => Expect::array(), 30 | 'feed' => Expect::type( 'bool|array' )->nullable(), 31 | 'sitemap' => Expect::bool( true ), 32 | 'date_archives' => Expect::bool( false ), 33 | 'time_archives' => Expect::bool( false ), 34 | 'taxonomy' => Expect::bool( false ), 35 | 'term_collect' => Expect::string()->nullable(), 36 | 'term_collection' => Expect::array(), 37 | 'routing' => Expect::type( 'false|array' )->default( [] ), 38 | 39 | // deprecated 40 | 'url_paths' => Expect::arrayOf( 'string', 'string' )->nullable(), 41 | 'uri' => Expect::string()->nullable(), 42 | 'uri_single' => Expect::string()->nullable(), 43 | 'routes' => Expect::arrayOf( 'string', 'string' ) 44 | ] ), 'string' ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Core/Schemas/Markdown.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Schemas; 13 | 14 | use Nette\Schema\Expect; 15 | use Nette\Schema\Schema; 16 | 17 | class Markdown 18 | { 19 | /** 20 | * Returns the schema structure. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public static function schema(): Schema 25 | { 26 | return Expect::structure( [ 27 | 'config' => Expect::array( [] ), 28 | 'extensions' => Expect::listOf( 'string' ), 29 | 'inline_parsers' => Expect::listOf( 'string' ) 30 | ] ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Core/Schemas/Template.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Core\Schemas; 13 | 14 | use Nette\Schema\Expect; 15 | use Nette\Schema\Schema; 16 | 17 | class Template 18 | { 19 | /** 20 | * Returns the schema structure. 21 | * 22 | * @since 1.0.0 23 | */ 24 | public static function schema(): Schema 25 | { 26 | return Expect::structure( [ 27 | 'tags' => Expect::array( [] ) 28 | ] ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Core/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 11 | * @link https://github.com/blush-dev/framework 12 | * @license https://opensource.org/licenses/MIT 13 | */ 14 | 15 | namespace Blush\Core; 16 | 17 | use Blush\Contracts\Bootable; 18 | use Blush\Contracts\Core\Application; 19 | 20 | abstract class ServiceProvider implements Bootable 21 | { 22 | /** 23 | * Accepts the application and sets it to the `$app` property. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function __construct( protected Application $app ) {} 28 | 29 | /** 30 | * Callback executed when the `Application` class registers providers. 31 | * 32 | * @since 1.0.0 33 | */ 34 | public function register(): void {} 35 | 36 | /** 37 | * Callback executed after all the service providers have been registered. 38 | * This is particularly useful for single-instance container objects that 39 | * only need to be loaded once per page and need to be resolved early. 40 | * 41 | * @since 1.0.0 42 | */ 43 | public function boot(): void {} 44 | } 45 | -------------------------------------------------------------------------------- /src/Feed/Writer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Feed; 13 | 14 | use Blush\Core\Proxies\Config; 15 | use Blush\Contracts\Content\{ContentEntry, ContentQuery}; 16 | use Blush\Contracts\Feed\FeedWriter; 17 | use Blush\Tools\Collection; 18 | 19 | class Writer implements FeedWriter 20 | { 21 | /** 22 | * Feed title. 23 | * 24 | * @since 1.0.0 25 | */ 26 | protected string $title = ''; 27 | 28 | /** 29 | * Webpage URL. 30 | * 31 | * @since 1.0.0 32 | */ 33 | protected string $url = ''; 34 | 35 | /** 36 | * Feed feed URL. 37 | * 38 | * @since 1.0.0 39 | */ 40 | protected string $feed_url = ''; 41 | 42 | /** 43 | * Feed description. 44 | * 45 | * @since 1.0.0 46 | */ 47 | protected string $description = ''; 48 | 49 | /** 50 | * Feed language. 51 | * 52 | * @since 1.0.0 53 | * @todo Map this to config. 54 | */ 55 | protected string $language = 'en-US'; 56 | 57 | /** 58 | * Feed copyright. 59 | * 60 | * @since 1.0.0 61 | */ 62 | protected ?string $copyright = null; 63 | 64 | /** 65 | * Feed published datetime. 66 | * 67 | * @since 1.0.0 68 | */ 69 | protected ?string $published = null; 70 | 71 | /** 72 | * Feed updated datetime. 73 | * 74 | * @since 1.0.0 75 | */ 76 | protected ?string $updated = null; 77 | 78 | /** 79 | * Feed image. 80 | * 81 | * @todo Not yet implemented. 82 | * @since 1.0.0 83 | */ 84 | protected ?string $image = null; 85 | 86 | /** 87 | * Feed favicon. 88 | * 89 | * @todo Not yet implemented. 90 | * @since 1.0.0 91 | */ 92 | protected ?string $favicon = null; 93 | 94 | /** 95 | * Feed Time To Live (TTL). 96 | * 97 | * @since 1.0.0 98 | */ 99 | protected int $ttl = 60; 100 | 101 | /** 102 | * Sets up the object state. 103 | * 104 | * @since 1.0.0 105 | */ 106 | public function __construct( 107 | protected ContentEntry $single, 108 | protected ContentQuery $collection, 109 | protected string $type = 'rss2' 110 | ) 111 | { 112 | $is_home = $this->single->type()->isHomeAlias(); 113 | 114 | $this->title = $is_home ? Config::get( 'app.title' ) : $single->title(); 115 | $this->description = $is_home ? Config::get( 'app.tagline' ) : $single->excerpt(); 116 | $this->url = $single->type()->url(); 117 | $this->feed_url = $single->type()->feedUrl(); 118 | $this->language = 'en-US'; 119 | $this->ttl = 60; 120 | 121 | // Get the first entry in the collection to set the published 122 | // and updated datetimes for the channel. 123 | if ( $first = $collection->first() ) { 124 | $format = 'atom' === $this->type ? DATE_ATOM : DATE_RSS; 125 | 126 | $this->published = $first->published( $format ) ?: null; 127 | $this->updated = $first->updated( $format ) ?: null; 128 | } 129 | } 130 | 131 | /** 132 | * Returns the Feed title. 133 | * 134 | * @since 1.0.0 135 | */ 136 | public function title(): string 137 | { 138 | return $this->title; 139 | } 140 | 141 | /** 142 | * Returns the Feed webpage URL. 143 | * 144 | * @since 1.0.0 145 | */ 146 | public function url(): string 147 | { 148 | return $this->url; 149 | } 150 | 151 | /** 152 | * Returns the Feed feed URL. 153 | * 154 | * @since 1.0.0 155 | */ 156 | public function feedUrl(): string 157 | { 158 | return $this->feed_url; 159 | } 160 | 161 | /** 162 | * Returns the Feed description. 163 | * 164 | * @since 1.0.0 165 | */ 166 | public function description(): string 167 | { 168 | return $this->description; 169 | } 170 | 171 | /** 172 | * Returns the Feed language. 173 | * 174 | * @since 1.0.0 175 | */ 176 | public function language(): string 177 | { 178 | return $this->language; 179 | } 180 | 181 | /** 182 | * Returns the Feed TTL. 183 | * 184 | * @since 1.0.0 185 | */ 186 | public function copyright(): ?string 187 | { 188 | return $this->copyright; 189 | } 190 | 191 | /** 192 | * Returns the feed published datetime. 193 | * 194 | * @since 1.0.0 195 | */ 196 | public function published(): ?string 197 | { 198 | return $this->published; 199 | } 200 | 201 | /** 202 | * Returns the feed updated datetime. 203 | * 204 | * @since 1.0.0 205 | */ 206 | public function updated(): ?string 207 | { 208 | return $this->updated; 209 | } 210 | 211 | /** 212 | * Returns the Feed TTL. 213 | * 214 | * @since 1.0.0 215 | */ 216 | public function ttl(): int 217 | { 218 | return $this->ttl; 219 | } 220 | 221 | /** 222 | * Returns the collection. 223 | * 224 | * @since 1.0.0 225 | */ 226 | public function collection(): ContentQuery 227 | { 228 | return $this->collection; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Markdown/ImageRenderer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Markdown; 13 | 14 | use Blush\Tools\{Media, Str}; 15 | use League\CommonMark\Extension\CommonMark\Node\Inline\{Image, Link}; 16 | use League\CommonMark\Node\Node; 17 | use League\CommonMark\Node\Inline\Text; 18 | use League\CommonMark\Renderer\{ChildNodeRendererInterface, NodeRendererInterface}; 19 | use League\CommonMark\Util\HtmlElement; 20 | 21 | class ImageRenderer implements NodeRendererInterface 22 | { 23 | public function __construct( protected ?Node $parent = null ) {} 24 | 25 | /** 26 | * Renders the element. 27 | * 28 | * @since 1.0.0 29 | */ 30 | public function render( Node $node, ChildNodeRendererInterface $childRenderer ) 31 | { 32 | $url = $node->getUrl(); 33 | $alt = ''; 34 | $figcaption = ''; 35 | $attr = []; 36 | 37 | $media = new Media( $url ); 38 | 39 | if ( 1 === count( $node->children() ) && $node->firstChild() instanceof Text ) { 40 | $alt = $childRenderer->renderNodes( $node->children() ); 41 | } 42 | 43 | $parent = $this->parent ?? $node->parent(); 44 | 45 | // Get attributes from `` element if they exist. 46 | if ( ! empty( $node->data['attributes'] ) ) { 47 | $attr = $node->data['attributes']; 48 | 49 | // If the `` parent is a link, try its attributes. 50 | } elseif ( $parent instanceof Link ) { 51 | if ( ! empty( $parent->data['attributes'] ) ) { 52 | $attr = $parent->data['attributes']; 53 | $parent->data['attributes'] = []; 54 | } 55 | } 56 | 57 | $args = [ 58 | 'src' => e( $url ), 59 | 'alt' => e( $alt ) 60 | ]; 61 | 62 | if ( $media->isValid() ) { 63 | $args['src'] = e( $media->url() ); 64 | $args['width'] = e( $media->width() ); 65 | $args['height'] = e( $media->height() ); 66 | } 67 | 68 | if ( isset( $attr['srcset'] ) ) { 69 | $args['srcset'] = $attr['srcset']; 70 | unset( $attr['srcset'] ); 71 | } 72 | 73 | if ( isset( $attr['sizes'] ) ) { 74 | $args['sizes'] = $attr['sizes']; 75 | unset( $attr['sizes'] ); 76 | } 77 | 78 | $image = new HtmlElement( 'img', $args, '', true ); 79 | 80 | if ( $this->parent instanceof Link ) { 81 | $url = $this->parent->getUrl(); 82 | 83 | if ( Str::startsWith( $url, '/' ) ) { 84 | $url = Str::appendUri( url( $url ) ); 85 | } 86 | 87 | $image = new HtmlElement( 'a', [ 88 | 'href' => e( $url ) 89 | ], $image ); 90 | } 91 | 92 | if ( $node->getTitle() ) { 93 | $figcaption = new HtmlElement( 94 | 'figcaption', 95 | [], 96 | $node->getTitle() 97 | ); 98 | } 99 | 100 | return new HtmlElement( 101 | 'figure', 102 | $attr, 103 | "{$image}\n{$figcaption}" 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Markdown/LinkRenderer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Markdown; 13 | 14 | use Blush\Tools\Str; 15 | use League\CommonMark\Extension\CommonMark\Node\Inline\{Image, Link}; 16 | use League\CommonMark\Node\Node; 17 | use League\CommonMark\Renderer\{ChildNodeRendererInterface, NodeRendererInterface}; 18 | use League\CommonMark\Util\HtmlElement; 19 | 20 | class LinkRenderer implements NodeRendererInterface 21 | { 22 | /** 23 | * Renders the element. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function render( Node $node, ChildNodeRendererInterface $childRenderer ) 28 | { 29 | $url = $node->getUrl(); 30 | 31 | if ( Str::startsWith( $url, '/' ) ) { 32 | $url = Str::appendUri( url( $url ) ); 33 | } 34 | 35 | $attr = $node->data['attributes'] ?? []; 36 | 37 | $attr['href'] = e( $url ); 38 | 39 | if ( $title = $node->getTitle() ) { 40 | $attr['title'] = e( $title ); 41 | } 42 | 43 | if ( 1 === count( $node->children() ) && $node->firstChild() instanceof Image ) { 44 | return ( new ImageRenderer( $node ) )->render( $node->firstChild(), $childRenderer ); 45 | } 46 | 47 | 48 | $innerHtml = $childRenderer->renderNodes( $node->children() ); 49 | 50 | return new HtmlElement( 'a', $attr, $innerHtml ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Markdown/ParagraphRenderer.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Markdown; 13 | 14 | use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; 15 | use League\CommonMark\Extension\CommonMark\Node\Inline\{Image, Link}; 16 | use League\CommonMark\Node\Node; 17 | use League\CommonMark\Renderer\{ChildNodeRendererInterface, NodeRendererInterface}; 18 | use League\CommonMark\Util\HtmlElement; 19 | 20 | class ParagraphRenderer implements NodeRendererInterface 21 | { 22 | /** 23 | * Renders the element. 24 | * 25 | * @since 1.0.0 26 | */ 27 | public function render( Node $node, ChildNodeRendererInterface $childRenderer ) 28 | { 29 | $innerHtml = $childRenderer->renderNodes( $node->children() ); 30 | 31 | if ( $node->parent() instanceof ListItem && 1 === count( $node->parent()->children() ) ) { 32 | return $innerHtml; 33 | } 34 | 35 | // Don't wrap images with

tags. 36 | if ( 1 === count( $node->children() ) ) { 37 | $child = $node->firstChild(); 38 | if ( $child instanceof Image ) { 39 | return $innerHtml; 40 | } 41 | 42 | if ( $child instanceof Link && 1 === count( $child->children() ) ) { 43 | $link_child = $node->firstChild(); 44 | if ( $link_child instanceof Image ) { 45 | return $innerHtml; 46 | } 47 | } 48 | } 49 | 50 | return new HtmlElement( 51 | 'p', 52 | $node->data['attributes'] ?? [], 53 | $innerHtml 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Markdown/Parser.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Markdown; 13 | 14 | use Blush\Contracts\Markdown\Parser as ParserContract; 15 | use Blush\Tools\Str; 16 | use League\CommonMark\ConverterInterface; 17 | 18 | class Parser implements ParserContract 19 | { 20 | /** 21 | * Stores content. 22 | * 23 | * @since 1.0.0 24 | */ 25 | protected string $content; 26 | 27 | /** 28 | * Stores front matter. 29 | * 30 | * @since 1.0.0 31 | */ 32 | protected array $front_matter; 33 | 34 | /** 35 | * Sets up object state. 36 | * 37 | * @since 1.0.0 38 | */ 39 | public function __construct( protected ConverterInterface $converter ) {} 40 | 41 | /** 42 | * Converts Markdown to HTML. 43 | * 44 | * @since 1.0.0 45 | */ 46 | public function convert( string $content ): self 47 | { 48 | $this->front_matter = []; 49 | 50 | $match = Str::captureFrontMatter( $content ); 51 | 52 | if ( $match ) { 53 | $this->front_matter = Str::yaml( $match ); 54 | $content = Str::trimFrontMatter( $content ); 55 | } 56 | 57 | $this->content = $this->converter->convert( 58 | $content 59 | )->getContent(); 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * Returns Markdown HTML. 66 | * 67 | * @since 1.0.0 68 | */ 69 | public function content(): string 70 | { 71 | return $this->content; 72 | } 73 | 74 | /** 75 | * Returns YAML front matter. 76 | * 77 | * @since 1.0.0 78 | */ 79 | public function frontMatter(): array 80 | { 81 | return $this->front_matter; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Messenger/Message.php: -------------------------------------------------------------------------------- 1 | 13 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 14 | * @link https://github.com/blush-dev/framework 15 | * @license https://opensource.org/licenses/MIT 16 | */ 17 | 18 | namespace Blush\Messenger; 19 | 20 | use Blush\Tools\Str; 21 | 22 | class Message 23 | { 24 | /** 25 | * Message to output. 26 | * 27 | * @since 1.0.0 28 | */ 29 | protected string $message = ''; 30 | 31 | /** 32 | * Type of message. Appended as a class of `.blush-message--{$type}`. 33 | * The default supported types are `note`, `success`, `warning`, and 34 | * `error`. 35 | * 36 | * @since 1.0.0 37 | */ 38 | protected string $type = 'note'; 39 | 40 | /** 41 | * Makes a new message. 42 | * 43 | * @since 1.0.0 44 | */ 45 | public function make( string $message, string $type = 'note' ): self 46 | { 47 | $message = trim( $message ); 48 | $message = ! Str::startsWith( $message, '<' ) ? "

{$message}

" : $message; 49 | 50 | $this->message = $message; 51 | $this->type = $type; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Returns the message and styles as HTML. 58 | * 59 | * @since 1.0.0 60 | */ 61 | public function render(): string 62 | { 63 | $message = sprintf( 64 | '
%s
', 65 | e( $this->type ?: 'note' ), 66 | $this->message 67 | ); 68 | 69 | $styles = str_replace( [ "\t", "\n", "\r", "\s\s", " " ], '', $this->styles() ); 70 | 71 | return $message . $styles; 72 | } 73 | 74 | /** 75 | * Displays the message and styles HTML. 76 | * 77 | * @since 1.0.0 78 | */ 79 | public function display(): void 80 | { 81 | echo $this->render(); 82 | } 83 | 84 | /** 85 | * Alias for `display()`. 86 | * 87 | * @since 1.0.0 88 | */ 89 | public function dump(): void 90 | { 91 | echo $this->display(); 92 | } 93 | 94 | /** 95 | * Dumps the HTML and dies. 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function dd(): void 100 | { 101 | $this->dump(); 102 | die(); 103 | } 104 | 105 | /** 106 | * Returns the CSS stylesheet. 107 | * 108 | * @since 1.0.0 109 | */ 110 | protected function styles(): string 111 | { 112 | return ''; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Routing/Component.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Routing; 13 | 14 | // Contracts. 15 | use Blush\Contracts\Bootable; 16 | use Blush\Contracts\Content\ContentTypes; 17 | use Blush\Contracts\Routing\RoutingRoutes; 18 | 19 | // Classes. 20 | use Blush\Core\Proxies\Config; 21 | use Blush\Controllers; 22 | use Blush\Tools\Str; 23 | 24 | class Component implements Bootable 25 | { 26 | /** 27 | * Sets up the object state. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function __construct( 32 | protected RoutingRoutes $routes, 33 | protected ContentTypes $types 34 | ) {} 35 | 36 | /** 37 | * Bootstraps the component. 38 | * 39 | * @since 1.0.0 40 | */ 41 | public function boot() : void 42 | { 43 | // Get the homepage alias if it exists. 44 | $alias = Config::get( 'app.home_alias' ); 45 | 46 | // Sort the content types. 47 | $types = array_reverse( $this->types->sortByPath() ); 48 | 49 | // Loop through the content types and add their routes. 50 | foreach ( $types as $type ) { 51 | 52 | // Skip if the content type doesn't support routing. 53 | if ( ! $type->hasRouting() ) { 54 | continue; 55 | } 56 | 57 | if ( $prefix = $type->routingPrefix() ) { 58 | $this->routes->addGroup( $prefix, $type->routes() ); 59 | continue; 60 | } 61 | 62 | foreach ( $type->routes() as $uri => $args ) { 63 | $this->routes->add( $uri, $args ); 64 | } 65 | } 66 | 67 | // Add homepage feed and paged routes if we have content type alias. 68 | if ( $alias && $this->types->has( $alias ) ) { 69 | 70 | if ( $this->types->get( $alias )->hasFeed() ) { 71 | $this->routes->add( 'feed/atom', [ 72 | 'name' => 'home.feed.atom', 73 | 'controller' => Controllers\CollectionFeedAtom::class 74 | ] ); 75 | 76 | $this->routes->add( 'feed', [ 77 | 'name' => 'home.feed', 78 | 'controller' => Controllers\CollectionFeed::class 79 | ] ); 80 | } 81 | 82 | $this->routes->add( 'page/{page}', [ 83 | 'name' => 'home.paged', 84 | 'controller' => Controllers\Home::class 85 | ] ); 86 | } 87 | 88 | // Add sitemap routes if supported. 89 | if ( Config::get( 'app.sitemap' ) ) { 90 | $this->routes->add( 'sitemap/{type}', [ 91 | 'name' => "sitemap", 92 | 'controller' => Controllers\Sitemap::class 93 | ] ); 94 | 95 | $this->routes->add( 'sitemap', [ 96 | 'name' => 'sitemapindex', 97 | 'controller' => Controllers\SitemapIndex::class 98 | ] ); 99 | } 100 | 101 | // Add homepage route. 102 | $this->routes->add( '/', [ 103 | 'name' => 'home', 104 | 'controller' => Controllers\Home::class 105 | ] ); 106 | 107 | // Add cache purge route for individual stores. 108 | $this->routes->add( 'purge/cache/{name}/{key}', [ 109 | 'name' => 'purge.cache.store', 110 | 'controller' => Controllers\Cache::class 111 | ] ); 112 | 113 | // Add cache purge route for all stores. 114 | $this->routes->add( 'purge/cache/{key}', [ 115 | 'name' => 'purge.cache', 116 | 'controller' => Controllers\Cache::class 117 | ] ); 118 | 119 | // Add catchall page route. 120 | $this->routes->add( '{*}', [ 121 | 'name' => 'page.single', 122 | 'controller' => Controllers\SinglePage::class 123 | ] ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Routing/Route/Routes.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Routing\Route; 13 | 14 | use Blush\Core\Proxies\App; 15 | use Blush\Contracts\Routing\{RoutingRoute, RoutingRoutes}; 16 | use Blush\Tools\Collection; 17 | 18 | class Routes extends Collection implements RoutingRoutes 19 | { 20 | /** 21 | * Holds an array of the route objects by name. 22 | * 23 | * @since 1.0.0 24 | */ 25 | protected array $named_routes = []; 26 | 27 | /** 28 | * Stores an array of route groups. 29 | * 30 | * @since 1.0.0 31 | */ 32 | protected array $groups = []; 33 | 34 | /** 35 | * Add a route. 36 | * 37 | * @since 1.0.0 38 | */ 39 | public function add( mixed $uri, mixed $options = [] ): void 40 | { 41 | parent::add( $uri, App::make( 'routing.route', [ 42 | 'uri' => $uri, 43 | 'options' => $options 44 | ] ) ); 45 | 46 | $route = $this->get( $uri )->make(); 47 | 48 | $this->named_routes[ $route->getName() ] = $route; 49 | } 50 | 51 | /** 52 | * Adds a new route group. 53 | * 54 | * @since 1.0.0 55 | */ 56 | public function addGroup( string $name, array $routes = [] ): void 57 | { 58 | $this->groups[ $name ] = []; 59 | 60 | $prefix = trim( $name, '/' ); 61 | 62 | foreach ( $routes as $uri => $options ) { 63 | $uri = trim( $uri, '/' ); 64 | $uri = $uri ? "{$prefix}/{$uri}" : $prefix; 65 | 66 | if ( $uri ) { 67 | $this->add( $uri, $options ); 68 | $this->groups[ $name ][] = $this->get( $uri ); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Returns the route groups. 75 | * 76 | * @since 1.0.0 77 | */ 78 | public function groups(): array 79 | { 80 | return $this->groups; 81 | } 82 | 83 | /** 84 | * Returns route by name. 85 | * 86 | * @since 1.0.0 87 | */ 88 | public function getNamedRoute( string $name ): ?RoutingRoute 89 | { 90 | return $this->named_routes[ $name ] ?? null; 91 | } 92 | 93 | /** 94 | * Returns an array of all routes with their names as the keys and the 95 | * Route objects as the values. 96 | * 97 | * @since 1.0.0 98 | */ 99 | public function getRoutesByName(): array 100 | { 101 | if ( $this->named_routes ) { 102 | return $this->named_routes; 103 | } 104 | 105 | foreach ( $this->all() as $route ) { 106 | $this->named_routes[ $route->getName() ] = $route; 107 | } 108 | 109 | return $this->named_routes; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Routing/Router.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Routing; 13 | 14 | use Blush\Contracts\Routing\{RoutingRoute, RoutingRoutes, RoutingRouter}; 15 | 16 | use Blush\Core\Proxies\{Cache, Config}; 17 | use Blush\Controllers\Error404; 18 | use Blush\Tools\Str; 19 | use Symfony\Component\HttpFoundation\{Request, Response}; 20 | 21 | class Router implements RoutingRouter 22 | { 23 | /** 24 | * HTTP Request. 25 | * 26 | * @since 1.0.0 27 | */ 28 | protected Request $request; 29 | 30 | /** 31 | * Sets up the object state. 32 | * 33 | * @since 1.0.0 34 | */ 35 | public function __construct( protected RoutingRoutes $routes ) 36 | { 37 | $this->request = Request::createFromGlobals(); 38 | } 39 | 40 | /** 41 | * Returns the HTTP request. 42 | * 43 | * @since 1.0.0 44 | */ 45 | public function request(): Request 46 | { 47 | return $this->request; 48 | } 49 | 50 | /** 51 | * Returns the request path. 52 | * 53 | * @since 1.0.0 54 | */ 55 | public function path(): string 56 | { 57 | return $this->request->getPathInfo(); 58 | } 59 | 60 | /** 61 | * Returns a cached HTTP Response if global caching is enabled. If not, 62 | * returns a new HTTP Response. 63 | * 64 | * @since 1.0.0 65 | */ 66 | public function response(): Response 67 | { 68 | // Just return the response if global caching is disabled. 69 | if ( ! Config::get( 'cache.global' ) ) { 70 | return $this->getResponse(); 71 | } 72 | 73 | // Trim slashes from the path. 74 | $path = Str::trimSlashes( $this->path() ); 75 | 76 | // Get excluded paths. 77 | $exclude = array_merge( [ 78 | 'feed', 79 | 'purge/cache' 80 | ], Config::get( 'cache.global_exclude' ) ); 81 | 82 | // Don't cache excluded pages. Just return response. 83 | foreach ( (array) $exclude as $_path ) { 84 | if ( Str::startsWith( $path, $_path ) ) { 85 | return $this->getResponse(); 86 | } 87 | } 88 | 89 | // If the path is empty, name it 'index'. 90 | if ( ! $path ) { 91 | $path = 'index'; 92 | } 93 | 94 | $cache_key = str_replace( [ '/', '\\' ], '.', $path ); 95 | $content = Cache::get( "global.{$cache_key}" ); 96 | $response = false; 97 | 98 | // If no cached content, get a new response and cache it. 99 | if ( ! $content ) { 100 | $response = $this->getResponse(); 101 | 102 | // Only cache status 200 content. 103 | // @todo Add cache config to for status to cache or not. 104 | if ( $response->statusOk() ) { 105 | $content = $response->getContent(); 106 | 107 | Cache::put( 108 | "global.{$cache_key}", 109 | $content, 110 | Config::get( 'cache.expires' ) 111 | ); 112 | } 113 | } 114 | 115 | // If no response is set, add the cached content to new response. 116 | if ( ! $response ) { 117 | $response = new Response(); 118 | $response->setContent( $content ); 119 | } 120 | 121 | // Return HTTP response. 122 | return $response; 123 | } 124 | 125 | /** 126 | * Returns an HTTP response. 127 | * 128 | * @since 1.0.0 129 | */ 130 | private function getResponse(): Response 131 | { 132 | $path = $this->path(); 133 | 134 | // Trim slashes unless homepage. 135 | if ( '/' !== $path ) { 136 | $path = Str::trimSlashes( $path ); 137 | } 138 | 139 | // Check for route that is an exact match for the request. This 140 | // will match route URIs that do not have variables, so we can 141 | // just return the matched route controller here. 142 | if ( $this->routes->has( $path ) ) { 143 | return $this->routes->get( $path )->callback( [ 144 | 'path' => $path 145 | ], $this->request() ); 146 | } 147 | 148 | // Loops through the route groups first. This allows us to 149 | // avoid any unnecessary pattern checks for routes that will 150 | // never match, at least if the current request happens to match 151 | // a registered group. 152 | foreach ( $this->routes->groups() as $group => $grouped_routes ) { 153 | 154 | if ( ! Str::startsWith( $path, "{$group}/" ) ) { 155 | continue; 156 | } 157 | 158 | // We have a group match, so let's test to see if any of 159 | // its routes match the current request. 160 | if ( $response = $this->locateRoute( $grouped_routes, $path ) ) { 161 | return $response; 162 | } 163 | } 164 | 165 | // Test all routes for matches if we get to this point. 166 | if ( $response = $this->locateRoute( $this->routes, $path ) ) { 167 | return $response; 168 | } 169 | 170 | // If nothing is found, send 404. 171 | return ( new Error404() )->__invoke( [ 172 | 'path' => $path 173 | ], $this->request() ); 174 | } 175 | 176 | /** 177 | * Helper method for locating a route from an array of potential matches 178 | * when compared to a path string. 179 | * 180 | * @since 1.0.0 181 | */ 182 | private function locateRoute( iterable $routes, string $path ): Response|false 183 | { 184 | // Loops through all routes and try to match them based on the 185 | // params contained in the route URI. 186 | foreach ( $routes as $route ) { 187 | 188 | // Skip routes without params. 189 | if ( ! Str::contains( $route->uri(), '{' ) ) { 190 | continue; 191 | } 192 | 193 | // Checks for matches against the route regex pattern, 194 | // e.g., `/path/{var_a}/example/{var_b}`. 195 | // 196 | // Individual `{var}` patterns are defined in the 197 | // `Route` class. If we have a full pattern match, we 198 | // pull out the `{var}` instances and set them as params 199 | // and pass them to the route's controller. 200 | if ( @preg_match( $route->pattern(), $path, $matches ) ) { 201 | 202 | // Removes the full match from the array, which 203 | // matches the entire URI path. The leftover 204 | // matches are the parameter values. 205 | array_shift( $matches ); 206 | 207 | // Combines the route vars as array keys and the 208 | // matches as the values. 209 | $params = array_combine( 210 | $route->parameters(), 211 | $matches 212 | ); 213 | 214 | // If no path set, pass the request path. 215 | if ( ! isset( $params['path'] ) ) { 216 | $params['path'] = $path; 217 | } 218 | 219 | // Invoke the route callback. 220 | return $route->callback( $params, $this->request() ); 221 | } 222 | } 223 | 224 | return false; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Routing/Url.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 13 | * @link https://github.com/blush-dev/framework 14 | * @license https://opensource.org/licenses/MIT 15 | */ 16 | 17 | namespace Blush\Routing; 18 | 19 | use Stringable; 20 | use Blush\Core\Proxies\App; 21 | use Blush\Contracts\Makeable; 22 | use Blush\Contracts\Routing\{RoutingRoutes, RoutingUrl}; 23 | 24 | class Url implements Makeable, RoutingUrl, Stringable 25 | { 26 | /** 27 | * Sets up the object state. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function __construct( protected RoutingRoutes $routes ) {} 32 | 33 | /** 34 | * Return the object instance. 35 | * 36 | * @since 1.0.0 37 | */ 38 | public function make(): self 39 | { 40 | return $this; 41 | } 42 | 43 | /** 44 | * Return the app URL and append an optional path. 45 | * 46 | * @since 1.0.0 47 | */ 48 | public function to( string $append = '' ): string 49 | { 50 | return App::url( append: $append ); 51 | } 52 | 53 | /** 54 | * Returns a route's URL. For variable routes, the params should be 55 | * passed in via the `$params` array in key/value pairs like: 56 | * `[ 'number' => 10 ]`. 57 | * 58 | * @since 1.0.0 59 | */ 60 | public function route( string $name, array $params = [] ): string 61 | { 62 | $path = false; 63 | 64 | if ( isset( $this->routes[ $name ] ) ) { 65 | $path = $this->routes[ $name ]->uri(); 66 | } elseif ( $route = $this->routes->getNamedRoute( $name ) ) { 67 | $path = $route->uri(); 68 | } 69 | 70 | return $path ? $this->to( 71 | $this->parseParams( $path, $params ) 72 | ) : ''; 73 | } 74 | 75 | /** 76 | * Accepts a path or URL string with possible `{param}` values in it. 77 | * Replaces the `{param}` strings with values from the `$params` array. 78 | * 79 | * @since 1.0.0 80 | */ 81 | public function parseParams( string $path, array $params = [] ): string 82 | { 83 | // Replace parameters with values. 84 | foreach ( $params as $param => $value ) { 85 | $path = str_replace( 86 | sprintf( '{%s}', $param ), 87 | $value, 88 | $path 89 | ); 90 | } 91 | 92 | return $path; 93 | } 94 | 95 | /** 96 | * When attempting to use the object as a string, return the result 97 | * of the `to()` method. 98 | * 99 | * @since 1.0.0 100 | */ 101 | public function __toString(): string 102 | { 103 | return $this->to(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Template/Component.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Template; 13 | 14 | // Contracts. 15 | use Blush\Contracts\Bootable; 16 | use Blush\Contracts\Template\TemplateTags; 17 | 18 | class Component implements Bootable 19 | { 20 | /** 21 | * Sets up the object state. 22 | * 23 | * @since 1.0.0 24 | */ 25 | public function __construct( 26 | protected TemplateTags $registry, 27 | protected array $tags 28 | ) {} 29 | 30 | /** 31 | * Bootstraps the component. 32 | * 33 | * @since 1.0.0 34 | */ 35 | public function boot(): void 36 | { 37 | foreach ( $this->tags as $name => $callback ) { 38 | $this->registry->add( $name, $callback ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Template/Engine.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Template; 13 | 14 | // Abstracts. 15 | use Blush\Contracts\Template\{ 16 | TemplateEngine, 17 | TemplateTag, 18 | TemplateTags, 19 | TemplateView 20 | }; 21 | 22 | // Concretes. 23 | use Blush\Core\Proxies\{App, Message}; 24 | use Blush\Tools\Collection; 25 | 26 | class Engine implements TemplateEngine 27 | { 28 | /** 29 | * Houses shared data to pass down to subviews. 30 | * 31 | * @since 1.0.0 32 | */ 33 | protected Collection $shared; 34 | 35 | /** 36 | * Sets up the object properties. 37 | * 38 | * @since 1.0.0 39 | */ 40 | public function __construct( protected TemplateTags $tags ) { 41 | $this->shared = new Collection(); 42 | } 43 | 44 | /** 45 | * Returns a template view. This should only be used for top-level views. 46 | * Otherwise, an error message is dumped and the process is stalled. 47 | * If including views within views, use `subview()` or one of its 48 | * several descendent methods included in this class. 49 | * 50 | * @since 1.0.0 51 | */ 52 | public function view( array|string $views, array|Collection $data = [] ): TemplateView 53 | { 54 | // If an array is passed in, call `first()`. 55 | if ( is_array( $views ) ) { 56 | return $this->first( $views, $data ); 57 | } 58 | 59 | // Assign the view name, which should be a string at this point. 60 | $name = $views; 61 | 62 | // @todo possible to assign this to $shared? 63 | $data = array_merge( 64 | $this->shared->all(), 65 | $data instanceof Collection ? $data->all() : $data 66 | ); 67 | 68 | // Always pass the engine back to the view. 69 | $data['engine'] = $this; 70 | 71 | $this->shared = new Collection( $data ); 72 | 73 | // Make a new template view. 74 | $view = App::make( 'template.view', [ 75 | 'name' => $views, 76 | 'data' => $data 77 | ] ); 78 | 79 | // Return template view. 80 | return $view; 81 | } 82 | 83 | /** 84 | * Checks if a view template exists. 85 | * 86 | * @since 1.0.0 87 | */ 88 | public function exists( string $name ): bool 89 | { 90 | $filename = str_replace( '.', '/', $name ); 91 | 92 | return file_exists( view_path( "{$filename}.php" ) ); 93 | } 94 | 95 | /** 96 | * Returns the first found view. 97 | * 98 | * @since 1.0.0 99 | */ 100 | public function first( array $views, array|Collection $data = [] ): TemplateView 101 | { 102 | foreach ( $views as $view ) { 103 | if ( $this->exists( $view ) ) { 104 | return $this->view( $view, $data ); 105 | } 106 | } 107 | 108 | Message::make( sprintf( 109 | '

Notice: View templates not found:

', 110 | implode( "\n", array_map( 111 | fn( $name ) => "
  • {$name}.php
  • ", 112 | $views 113 | ) ) 114 | ) )->dd(); 115 | } 116 | 117 | /** 118 | * Returns any found view. 119 | * 120 | * @since 1.0.0 121 | */ 122 | public function any( array $views, array|Collection $data = [] ): TemplateView|false 123 | { 124 | foreach ( (array) $views as $name ) { 125 | if ( $this->exists( $name ) ) { 126 | return $this->view( $name, $data ); 127 | } 128 | } 129 | 130 | return false; 131 | } 132 | 133 | /** 134 | * Includes a view. 135 | * 136 | * @since 1.0.0 137 | */ 138 | public function include( array|string $views, array|Collection $data = [] ): void 139 | { 140 | $this->first( (array) $views, $data )->display(); 141 | } 142 | 143 | /** 144 | * Includes a view only if it exists. No errors or warnings if no view 145 | * template is found. 146 | * 147 | * @since 1.0.0 148 | */ 149 | public function includeIf( array|string $views, array|Collection $data = [] ): void 150 | { 151 | if ( $view = $this->any( (array) $views, $data ) ) { 152 | $view->display(); 153 | } 154 | } 155 | 156 | /** 157 | * Includes a view when `$when` is `true`. 158 | * 159 | * @since 1.0.0 160 | */ 161 | public function includeWhen( 162 | mixed $when, 163 | array|string $views, 164 | array|Collection $data = [] 165 | ): void 166 | { 167 | if ( $when ) { 168 | $this->include( $views, $data ); 169 | } 170 | } 171 | 172 | /** 173 | * Includes a view unless `$unless` is `true`. 174 | * 175 | * @since 1.0.0 176 | */ 177 | public function includeUnless( 178 | mixed $unless, 179 | array|string $views, 180 | array|Collection $data = [] 181 | ): void 182 | { 183 | if ( ! $unless ) { 184 | $this->include( $views, $data ); 185 | } 186 | } 187 | 188 | /** 189 | * Loops through an array of items and includes a view for each. Use 190 | * the `$var` variable to set a variable name for the item when passed 191 | * to the view. Pass a fallback view name via `$empty` to show if 192 | * the items array is empty. 193 | * 194 | * @since 1.0.0 195 | */ 196 | public function each( 197 | array|string $views, 198 | iterable $items = [], 199 | string $var = '', 200 | array|string $empty = [] 201 | ): void 202 | { 203 | if ( ! $items && $empty ) { 204 | $this->include( $empty ); 205 | return; 206 | } 207 | 208 | foreach ( $items as $item ) { 209 | $this->include( 210 | $views, 211 | $var ? [ $var => $item ] : [] 212 | ); 213 | } 214 | } 215 | 216 | /** 217 | * Returns a template view. Use for getting views inside of other views. 218 | * This makes sure shared data is passed down to the subview. 219 | * 220 | * @since 1.0.0 221 | * @deprecated 1.0.0 222 | */ 223 | public function subview( array|string $views, array|Collection $data = [] ): TemplateView 224 | { 225 | return $this->view( $views, $data ); 226 | } 227 | 228 | /** 229 | * Returns a template tag object or null when it doesn't exist. 230 | * 231 | * @since 1.0.0 232 | */ 233 | public function tag( string $name, mixed ...$args ): ?TemplateTag 234 | { 235 | return $this->tags->callback( $name, $this->shared, $args ); 236 | } 237 | 238 | /** 239 | * Allows registered template tags to be used as methods. 240 | * 241 | * @since 1.0.0 242 | */ 243 | public function __call( string $name, array $arguments ): mixed 244 | { 245 | return $this->tag( $name, ...$arguments ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/Template/Hierarchy.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 13 | * @link https://github.com/blush-dev/framework 14 | * @license https://opensource.org/licenses/MIT 15 | */ 16 | 17 | namespace Blush\Template; 18 | 19 | use Blush\Contracts\Content\{ContentEntry, ContentType}; 20 | use Blush\Template\Feed\Feed; 21 | 22 | class Hierarchy 23 | { 24 | /** 25 | * Returns the default single template hierarchy. 26 | * 27 | * @since 1.0.0 28 | */ 29 | public static function single( ContentEntry $entry ): array 30 | { 31 | $entry_name = $entry->name(); 32 | $type_name = $entry->type()->name(); 33 | $model_name = static::modelName( $entry->type() ); 34 | 35 | return array_merge( $entry->viewPaths(), [ 36 | "single-{$type_name}-{$entry_name}", 37 | "single-{$type_name}", 38 | "single-{$model_name}", 39 | 'single', 40 | 'index' 41 | ] ); 42 | } 43 | 44 | /** 45 | * Returns the error 404 single template hierarchy. 46 | * 47 | * @todo Create an `error` content type. 48 | * @since 1.0.0 49 | */ 50 | public static function error404(): array 51 | { 52 | return [ 53 | 'single-error-404', 54 | 'single-error', 55 | 'single', 56 | 'index' 57 | ]; 58 | } 59 | 60 | /** 61 | * Returns the homepage single template hierarchy. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public static function singleHome( ContentEntry $entry ): array 66 | { 67 | $entry_name = $entry->name(); 68 | $type_name = $entry->type()->name(); 69 | $model_name = static::modelName( $entry->type() ); 70 | 71 | return array_merge( $entry->viewPaths(), [ 72 | 'single-home', 73 | "single-{$type_name}-{$entry_name}", 74 | "single-{$type_name}", 75 | "single-{$model_name}", 76 | 'single', 77 | 'index' 78 | ] ); 79 | } 80 | 81 | /** 82 | * Returns the default collection template hierarchy. 83 | * 84 | * @since 1.0.0 85 | */ 86 | public static function collection( ContentEntry $entry ): array 87 | { 88 | $type_name = $entry->type()->name(); 89 | $model_name = static::modelName( $entry->type() ); 90 | 91 | return [ 92 | "collection-{$type_name}", 93 | "collection-{$model_name}", 94 | 'collection', 95 | 'index' 96 | ]; 97 | } 98 | 99 | /** 100 | * Returns the homepage collection template hierarchy. 101 | * 102 | * @since 1.0.0 103 | */ 104 | public static function collectionHome( ContentEntry $entry ): array 105 | { 106 | return array_merge( [ 107 | 'collection-home' 108 | ], static::collection( $entry ) ); 109 | } 110 | 111 | /** 112 | * Returns the term collection template hierarchy. 113 | * 114 | * @since 1.0.0 115 | */ 116 | public static function collectionTerm( ContentEntry $entry ): array 117 | { 118 | $entry_name = $entry->name(); 119 | $type_name = $entry->type()->name(); 120 | 121 | return [ 122 | "collection-{$type_name}-{$entry_name}", 123 | "collection-{$type_name}", 124 | 'collection-term', 125 | 'collection', 126 | 'index' 127 | ]; 128 | } 129 | 130 | /** 131 | * Returns the date collection template hierarchy. 132 | * 133 | * @since 1.0.0 134 | */ 135 | public static function collectionDate( ContentType $type ): array 136 | { 137 | $type_name = $type->name(); 138 | 139 | return [ 140 | "collection-datetime-{$type_name}", 141 | "collection-{$type_name}", 142 | 'collection-datetime', 143 | 'collection', 144 | 'index' 145 | ]; 146 | } 147 | 148 | /** 149 | * Helper method for getting a content type's model name. This is a 150 | * precursor to a larger content-type blueprint object planned for the 151 | * future. 152 | * 153 | * @since 1.0.0 154 | */ 155 | protected static function modelName( ContentType $type ): string 156 | { 157 | return $type->isTaxonomy() ? 'taxonomy' : 'content'; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Template/Tag/DocumentTitle.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Template\Tag; 13 | 14 | // Contracts. 15 | use Blush\Contracts\{Displayable, Renderable}; 16 | 17 | // Concretes. 18 | use Blush\Core\Proxies\{App, Config}; 19 | use Blush\Tools\Str; 20 | 21 | class DocumentTitle implements Displayable, Renderable 22 | { 23 | /** 24 | * Stores the built document title. 25 | * 26 | * @since 1.0.0 27 | */ 28 | protected string $doctitle = ''; 29 | 30 | /** 31 | * View title. 32 | * 33 | * @since 1.0.0 34 | */ 35 | protected string $view_title = ''; 36 | 37 | /** 38 | * Page number for paged views. 39 | * 40 | * @since 1.0.0 41 | */ 42 | protected int $page = 1; 43 | 44 | /** 45 | * Separator string between doctitle items 46 | * 47 | * @since 1.0.0 48 | */ 49 | protected string $sep = '—'; 50 | 51 | /** 52 | * Sets up the object state. 53 | * 54 | * @since 1.0.0 55 | */ 56 | public function __construct( string $title = '', array $options = [] ) 57 | { 58 | $this->view_title = $title; 59 | 60 | if ( isset( $options['page'] ) ) { 61 | $this->page = abs( intval( $options['page'] ) ); 62 | } 63 | } 64 | 65 | /** 66 | * Returns the doctitle between `` tags. 67 | * 68 | * @since 1.0.0 69 | */ 70 | public function toHtml(): string 71 | { 72 | return $this->render(); 73 | } 74 | 75 | /** 76 | * Displays the doctitle. 77 | * 78 | * @since 1.0.0 79 | */ 80 | public function display(): void 81 | { 82 | echo $this->render(); 83 | } 84 | 85 | /** 86 | * Returns the doctitle. 87 | * 88 | * @since 1.0.0 89 | */ 90 | public function render(): string 91 | { 92 | if ( ! $this->doctitle ) { 93 | $this->doctitle = $this->build(); 94 | } 95 | 96 | return sprintf( '<title>%s', $this->doctitle ); 97 | } 98 | 99 | /** 100 | * Builds the doctitle. 101 | * 102 | * @since 1.0.0 103 | */ 104 | protected function build(): string 105 | { 106 | $app_title = Config::get( 'app.title' ); 107 | $app_tagline = Config::get( 'app.tagline' ); 108 | $paged = 2 <= $this->page; 109 | $items = []; 110 | 111 | $items['title'] = $this->view_title ? \e( $this->view_title ) : \e( $app_title ); 112 | 113 | if ( $paged ) { 114 | $items['title'] .= sprintf( ': Page %d', intval( $this->page ) ); 115 | } 116 | 117 | if ( $this->view_title ) { 118 | $items['app_title'] = \e( $app_title ); 119 | } 120 | 121 | if ( ! $this->view_title && ! $paged ) { 122 | $items['app_tagline'] = \e( $app_tagline ); 123 | } 124 | 125 | return implode( " {$this->sep} ", array_filter( $items ) ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Template/Tag/PoweredBy.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Template\Tag; 13 | 14 | // Contracts. 15 | use Blush\Contracts\{Displayable, Renderable}; 16 | 17 | class PoweredBy implements Displayable, Renderable 18 | { 19 | /** 20 | * Stores the array of notes. 21 | * 22 | * @since 1.0.0 23 | */ 24 | protected array $superpowers; 25 | 26 | /** 27 | * Sets up the object state. 28 | * 29 | * @since 1.0.0 30 | */ 31 | public function __construct() 32 | { 33 | $this->superpowers = [ 34 | 'Powered by heart and soul.', 35 | 'Powered by crazy ideas and passion.', 36 | 'Powered by the thing that holds all things together in the universe.', 37 | 'Powered by love.', 38 | 'Powered by the vast and endless void.', 39 | 'Powered by the code of a maniac.', 40 | 'Powered by peace and understanding.', 41 | 'Powered by coffee.', 42 | 'Powered by sleepless nights.', 43 | 'Powered by the love of all things.', 44 | 'Powered by something greater than myself.', 45 | // 2022-10-05 46 | 'Powered by elbow grease. Held together by tape and bubble gum.', 47 | 'Powered by an old mixtape and memories of lost love.', 48 | 'Powered by thoughts of old love letters.' 49 | ]; 50 | } 51 | 52 | /** 53 | * Displays the message. 54 | * 55 | * @since 1.0.0 56 | */ 57 | public function display(): void 58 | { 59 | echo $this->render(); 60 | } 61 | 62 | /** 63 | * Returns the message. 64 | * 65 | * @since 1.0.0 66 | */ 67 | public function render(): string 68 | { 69 | $collection = $this->superpowers; 70 | 71 | return $collection[ array_rand( $collection, 1 ) ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Template/Tag/Tag.php: -------------------------------------------------------------------------------- 1 | tagName()`). Developers can create custom constructors with any 8 | * number of parameters, required or not, and the engine will pass down those 9 | * that are input. Essentially, this is just a way of extending the template 10 | * engine for custom use cases. 11 | * 12 | * @package Blush 13 | * @author Justin Tadlock 14 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 15 | * @link https://github.com/blush-dev/framework 16 | * @license https://opensource.org/licenses/MIT 17 | */ 18 | 19 | namespace Blush\Template\Tag; 20 | 21 | use Stringable; 22 | use Blush\Contracts\{CastsToHtml, CastsToText}; 23 | use Blush\Contracts\Template\TemplateTag; 24 | use Blush\Tools\Collection; 25 | 26 | abstract class Tag implements CastsToHtml, CastsToText, Stringable, TemplateTag 27 | { 28 | /** 29 | * Shared data passed in from a view. 30 | * 31 | * @since 1.0.0 32 | */ 33 | protected Collection $data; 34 | 35 | /** 36 | * Developers must overload this method and return either an empty string 37 | * or valid HTML. The resulting output is expected to be safe for 38 | * output within a template. 39 | * 40 | * @since 1.0.0 41 | */ 42 | abstract public function toHtml(): string; 43 | 44 | /** 45 | * Developers must overload this method and return either an empty string 46 | * or the element's value as a string. If the tag is only ever meant to 47 | * output HTML (e.g., like a complex gallery implementation), returning 48 | * an empty string instead would be OK. The return value of this 49 | * method is not expected to be escaped and safe for display. This 50 | * should happen at the point of display. 51 | * 52 | * @since 1.0.0 53 | */ 54 | abstract public function toText(): string; 55 | 56 | /** 57 | * Sets the data for the tag. Generally, this is `View` data that is 58 | * automatically passed in from the `Engine`, at least when used in that 59 | * context. So, any data should be data normally available. However, 60 | * developers should perform checks on whether data exists and is valid 61 | * before using it. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function setData( Collection $data ): void 66 | { 67 | $this->data = $data; 68 | } 69 | 70 | /** 71 | * Returns the tag HTML if it is used directly as a string. 72 | * 73 | * @since 1.0.0 74 | */ 75 | public function __toString(): string 76 | { 77 | return $this->toHtml(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Template/Tag/Tags.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Template\Tag; 13 | 14 | use Blush\App; 15 | use Blush\Contracts\Template\{TemplateTag, TemplateTags}; 16 | use Blush\Tools\Collection; 17 | 18 | class Tags extends Collection implements TemplateTags 19 | { 20 | /** 21 | * Creates a new tag object if it exists. 22 | * 23 | * @since 1.0.0 24 | */ 25 | public function callback( 26 | string $name, 27 | Collection $data, 28 | array $args = [] 29 | ): ?TemplateTag 30 | { 31 | // Check if the tag is registered and that its class exists. 32 | if ( $this->has( $name ) && class_exists( $this->get( $name ) ) ) { 33 | $callback = $this->get( $name ); 34 | 35 | // Creates a new object from the registered tag class. 36 | $tag = new $callback( ...$args ); 37 | 38 | // Set the data before returning the tag. 39 | $tag->setData( $data ); 40 | return $tag; 41 | } 42 | 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Template/View.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Template; 13 | 14 | // Contracts. 15 | use Stringable; 16 | use Blush\Contracts\Template\TemplateView; 17 | 18 | // Concretes. 19 | use Blush\App; 20 | use Blush\Tools\Collection; 21 | 22 | class View implements TemplateView, Stringable 23 | { 24 | /** 25 | * An collection of data that is passed into the view template. 26 | * 27 | * @since 1.0.0 28 | */ 29 | protected Collection $data; 30 | 31 | /** 32 | * The template filename. 33 | * 34 | * @since 1.0.0 35 | */ 36 | protected ?string $template = null; 37 | 38 | /** 39 | * Sets up the view properties. 40 | * 41 | * @since 1.0.0 42 | */ 43 | public function __construct( protected string $name, array|Collection $data = [] ) 44 | { 45 | $this->name = str_replace( '/', '.', $this->name ); 46 | 47 | if ( ! $data instanceof Collection ) { 48 | $data = new Collection( (array) $data ); 49 | } 50 | 51 | $this->data = $data; 52 | } 53 | 54 | /** 55 | * Returns the located template. 56 | * 57 | * @since 1.0.0 58 | */ 59 | public function template(): string 60 | { 61 | if ( is_null( $this->template ) ) { 62 | $filename = str_replace( '.', '/', $this->name ); 63 | $this->template = view_path( "{$filename}.php" ); 64 | } 65 | 66 | return $this->template; 67 | } 68 | 69 | /** 70 | * Sets the view data. 71 | * 72 | * @since 1.0.0 73 | */ 74 | public function setData( Collection $data ): void 75 | { 76 | $this->data = $data; 77 | } 78 | 79 | /** 80 | * Gets the view data. 81 | * 82 | * @since 1.0.0 83 | */ 84 | public function getData(): Collection 85 | { 86 | return $this->data; 87 | } 88 | 89 | /** 90 | * Displays the view. 91 | * 92 | * @since 1.0.0 93 | */ 94 | public function display(): void 95 | { 96 | echo $this->render(); 97 | } 98 | 99 | /** 100 | * Returns the view. 101 | * 102 | * @since 1.0.0 103 | */ 104 | public function render(): string 105 | { 106 | if ( ! $this->template() ) { 107 | return ''; 108 | } 109 | 110 | // Extract the data into individual variables. Each of 111 | // these variables will be available in the template. 112 | extract( $this->data->all() ); 113 | 114 | // Make `$data` and `$view` variables available to templates. 115 | $data = $this->data; 116 | $view = $this; 117 | 118 | ob_start(); 119 | include $this->template(); 120 | return ob_get_clean(); 121 | } 122 | 123 | /** 124 | * When attempting to use the object as a string, return the template 125 | * output. 126 | * 127 | * @since 1.0.0 128 | */ 129 | public function __toString(): string 130 | { 131 | return $this->render(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Tools/Collection.php: -------------------------------------------------------------------------------- 1 | 12 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 13 | * @link https://github.com/blush-dev/framework 14 | * @license https://opensource.org/licenses/MIT 15 | */ 16 | 17 | namespace Blush\Tools; 18 | 19 | use ArrayObject; 20 | use JsonSerializable; 21 | 22 | class Collection extends ArrayObject implements JsonSerializable 23 | { 24 | /** 25 | * Add an item. 26 | * 27 | * @since 1.0.0 28 | */ 29 | public function add( mixed $name, mixed $value ): void 30 | { 31 | $this->offsetSet( $name, $value ); 32 | } 33 | 34 | /** 35 | * Removes an item. 36 | * 37 | * @since 1.0.0 38 | */ 39 | public function remove( mixed $name ): void 40 | { 41 | $this->offsetUnset( $name ); 42 | } 43 | 44 | /** 45 | * Checks if an item exists. 46 | * 47 | * @since 1.0.0 48 | */ 49 | public function has( mixed $name ): bool 50 | { 51 | return $this->offsetExists( $name ); 52 | } 53 | 54 | /** 55 | * Returns an item. 56 | * 57 | * @since 1.0.0 58 | */ 59 | public function get( mixed $name ) 60 | { 61 | return $this->offsetGet( $name ); 62 | } 63 | 64 | /** 65 | * Returns the collection of items. 66 | * 67 | * @since 1.0.0 68 | */ 69 | public function all(): array 70 | { 71 | return $this->getArrayCopy(); 72 | } 73 | 74 | /** 75 | * Magic method when trying to set a property. Assume the property is 76 | * part of the collection and add it. 77 | * 78 | * @since 1.0.0 79 | */ 80 | public function __set( string $name, mixed $value ): void 81 | { 82 | $this->offsetSet( $name, $value ); 83 | } 84 | 85 | /** 86 | * Magic method when trying to unset a property. 87 | * 88 | * @since 1.0.0 89 | */ 90 | public function __unset( string $name ): void 91 | { 92 | $this->offsetUnset( $name ); 93 | } 94 | 95 | /** 96 | * Magic method when trying to check if a property has. 97 | * 98 | * @since 1.0.0 99 | */ 100 | public function __isset( string $name ): bool 101 | { 102 | return $this->offsetExists( $name ); 103 | } 104 | 105 | /** 106 | * Magic method when trying to get a property. 107 | * 108 | * @since 1.0.0 109 | */ 110 | public function __get( string $name ): mixed 111 | { 112 | return $this->offSetGet( $name ); 113 | } 114 | 115 | /** 116 | * Returns a JSON-ready array of data. 117 | * 118 | * @since 1.0.0 119 | */ 120 | public function jsonSerialize(): mixed 121 | { 122 | return array_map( function( $value ) { 123 | 124 | if ( $value instanceof JsonSerializable ) { 125 | return $value->jsonSerialize(); 126 | } 127 | 128 | return $value; 129 | 130 | }, $this->all() ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Tools/Media.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Blush\Tools; 13 | 14 | class Media 15 | { 16 | /** 17 | * Directory path for the media file. 18 | * 19 | * @since 1.0.0 20 | */ 21 | protected string $path = ''; 22 | 23 | /** 24 | * URL path for the media file. 25 | * 26 | * @since 1.0.0 27 | */ 28 | protected string $url = ''; 29 | 30 | /** 31 | * Width of the media. 32 | * 33 | * @since 1.0.0 34 | */ 35 | protected int $width = 0; 36 | 37 | /** 38 | * Height of the media. 39 | * 40 | * @since 1.0.0 41 | */ 42 | protected int $height = 0; 43 | 44 | /** 45 | * Size (in bytes) of the media file. 46 | * 47 | * @since 1.0.0 48 | */ 49 | protected int $size = 0; 50 | 51 | /** 52 | * Mime type for the media file. 53 | * 54 | * @since 1.0.0 55 | */ 56 | protected string $mime_type = ''; 57 | 58 | /** 59 | * Sets up the object properties. The `$filepath` is expected to be 60 | * relative to the site root. However, we will attempt to find it 61 | * regardless of whether it's a full directory path or URL. 62 | * 63 | * @since 1.0.0 64 | */ 65 | public function __construct( string $filepath ) 66 | { 67 | // Strip the app directory and URL path if they prepend the file. 68 | $filepath = Str::afterFirst( $filepath, path() ); 69 | $filepath = Str::afterFirst( $filepath, url() ); 70 | 71 | if ( file_exists( path( $filepath ) ) ) { 72 | $this->path = path( $filepath ); 73 | $this->url = url( $filepath ); 74 | $this->size = filesize( $this->path ); 75 | $this->mime_type = mime_content_type( $this->path ); 76 | 77 | if ( $this->hasType( 'image' ) ) { 78 | $image = getimagesize( $this->path ); 79 | $this->width = $image[0]; 80 | $this->height = $image[1]; 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Conditional for checking if the media is considered valid. 87 | * 88 | * @since 1.0.0 89 | */ 90 | public function isValid(): bool 91 | { 92 | return $this->path && $this->url && $this->hasAllowedMimeType(); 93 | } 94 | 95 | /** 96 | * Conditional for checking if the media is considered valid. Alias for 97 | * `isValid()`. 98 | * 99 | * @since 1.0.0 100 | * @deprecated 1.0.0 101 | */ 102 | public function valid(): bool 103 | { 104 | return $this->isValid(); 105 | } 106 | 107 | /** 108 | * Returns the directory path for the media file. 109 | * 110 | * @since 1.0.0 111 | */ 112 | public function path(): string 113 | { 114 | return $this->path; 115 | } 116 | 117 | /** 118 | * Returns the URL for the media file. 119 | * 120 | * @since 1.0.0 121 | */ 122 | public function url(): string 123 | { 124 | return $this->url; 125 | } 126 | 127 | /** 128 | * Returns the width of the media. 129 | */ 130 | public function width(): int 131 | { 132 | return $this->width; 133 | } 134 | 135 | /** 136 | * Returns the height of the media. 137 | * 138 | * @since 1.0.0 139 | */ 140 | public function height(): int 141 | { 142 | return $this->height; 143 | } 144 | 145 | /** 146 | * Returns the media file size in bytes. 147 | * 148 | * @since 1.0.0 149 | */ 150 | public function size(): int 151 | { 152 | return $this->size; 153 | } 154 | 155 | /** 156 | * Returns the mime type. 157 | * 158 | * @since 1.0.0 159 | */ 160 | public function mimeType(): string 161 | { 162 | return $this->mime_type; 163 | } 164 | 165 | /** 166 | * Returns the media file type (e.g., image, audio, video). 167 | * 168 | * @since 1.0.0 169 | */ 170 | public function type(): string 171 | { 172 | if ( ! $mime = $this->mimeType() ) { 173 | return ''; 174 | } 175 | 176 | return Str::beforeFirst( $mime, '/' ); 177 | } 178 | 179 | /** 180 | * Returns the media file subtype (e.g., jpeg, mp4, mp3, etc.). 181 | * 182 | * @since 1.0.0 183 | */ 184 | public function subtype(): string 185 | { 186 | if ( ! $mime = $this->mimeType() ) { 187 | return ''; 188 | } 189 | 190 | return Str::afterFirst( $mime, '/' ); 191 | } 192 | 193 | /** 194 | * Conditional to check if the media file has a specific type (e.g., 195 | * image, audio, video). 196 | * 197 | * @since 1.0.0 198 | */ 199 | public function hasType( string $type = 'image' ): bool 200 | { 201 | return $type === $this->type(); 202 | } 203 | 204 | /** 205 | * Conditional check to see if the media has a specific subtype (e.g., 206 | * jpeg, mp4, mp3, etc.). 207 | * 208 | * @since 1.0.0 209 | */ 210 | public function hasSubtype( string $subtype = 'jpeg' ): bool 211 | { 212 | return $subtype === $this->subtype(); 213 | } 214 | 215 | /** 216 | * Conditional check to see if the media's mime type is allowed. 217 | * 218 | * @since 1.0.0 219 | */ 220 | public function hasAllowedMimeType(): bool 221 | { 222 | if ( ! $mime = $this->mimeType() ) { 223 | return false; 224 | } 225 | 226 | return in_array( $mime, $this->allowedMimeTypes() ); 227 | } 228 | 229 | /** 230 | * Returns an array of allowed mime types. 231 | * 232 | * @todo Flesh out full list of image, audio, and video mime types. 233 | * @since 1.0.0 234 | */ 235 | protected function allowedMimeTypes(): array 236 | { 237 | return [ 238 | // Images 239 | 'image/apng', 240 | 'image/avif', 241 | 'image/gif', 242 | 'image/jpeg', // .jpg|.jpeg 243 | 'image/png', 244 | 'image/svg+xml', 245 | 'image/webp', 246 | 247 | // Audio 248 | 'audio/mpeg', // .mp3 249 | 'audio/wav', 250 | 'audio/ogg', 251 | 252 | // Video 253 | 'video/mp4', 254 | 'video/ogg', 255 | 'video/webm' 256 | ]; 257 | } 258 | 259 | /** 260 | * If object is used as a string, return the media file's URL. This 261 | * should keep consistency with pre-1.0.0 implementations expecting a 262 | * URL instead of an object, particularly dealing with entry metadata. 263 | * 264 | * @since 1.0.0 265 | */ 266 | public function __toString(): string 267 | { 268 | return $this->url() ?: ''; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/functions-deprecated.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright Copyright (c) 2018 - 2022, Justin Tadlock 8 | * @link https://github.com/blush-dev/framework 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | if ( ! function_exists( 'uri' ) ) { 13 | /** 14 | * Returns app URI with optionsal appended path. 15 | * 16 | * @since 1.0.0 17 | */ 18 | function uri( string $append = '' ): string 19 | { 20 | return url( $append ); 21 | } 22 | } 23 | 24 | if ( ! function_exists( 'app_uri' ) ) { 25 | /** 26 | * Returns config URI with optional appended path/file. 27 | * 28 | * @since 1.0.0 29 | */ 30 | function app_uri( string $append = '' ): string 31 | { 32 | return app_url( $append ); 33 | } 34 | } 35 | 36 | if ( ! function_exists( 'config_uri' ) ) { 37 | /** 38 | * Returns config URI with optional appended path/file. 39 | * 40 | * @since 1.0.0 41 | */ 42 | function config_uri( string $append = '' ): string 43 | { 44 | return config_url( $append ); 45 | } 46 | } 47 | 48 | if ( ! function_exists( 'public_uri' ) ) { 49 | /** 50 | * Returns public URI with optional appended path/file. 51 | * 52 | * @since 1.0.0 53 | */ 54 | function public_uri( string $append = '' ): string 55 | { 56 | return public_url( $append ); 57 | } 58 | } 59 | 60 | if ( ! function_exists( 'view_uri' ) ) { 61 | /** 62 | * Returns view URI with optional appended path/file. 63 | * 64 | * @since 1.0.0 65 | */ 66 | function view_uri( string $append = '' ): string 67 | { 68 | return view_url( $append ); 69 | } 70 | } 71 | 72 | if ( ! function_exists( 'resource_uri' ) ) { 73 | /** 74 | * Returns resource URI with optional appended path/file. 75 | * 76 | * @since 1.0.0 77 | */ 78 | function resource_uri( string $append = '' ): string 79 | { 80 | return resource_url( $append ); 81 | } 82 | } 83 | 84 | if ( ! function_exists( 'storage_uri' ) ) { 85 | /** 86 | * Returns storage URI with optional appended path/file. 87 | * 88 | * @since 1.0.0 89 | */ 90 | function storage_uri( string $append = '' ): string 91 | { 92 | return storage_url( $append ); 93 | } 94 | } 95 | 96 | if ( ! function_exists( 'cache_uri' ) ) { 97 | /** 98 | * Returns cache URI with optional appended path/file. 99 | * 100 | * @since 1.0.0 101 | */ 102 | function cache_uri( string $append = '' ): string 103 | { 104 | return cache_url( $append ); 105 | } 106 | } 107 | 108 | if ( ! function_exists( 'user_uri' ) ) { 109 | /** 110 | * Returns public URI with optional appended path/file. 111 | * 112 | * @since 1.0.0 113 | */ 114 | function user_uri( string $append = '' ): string 115 | { 116 | return user_url( $append ); 117 | } 118 | } 119 | 120 | if ( ! function_exists( 'content_uri' ) ) { 121 | /** 122 | * Returns content URI with optional appended path/file. 123 | * 124 | * @since 1.0.0 125 | */ 126 | function content_uri( string $append = '' ): string 127 | { 128 | return content_url( $append ); 129 | } 130 | } 131 | 132 | if ( ! function_exists( 'media_uri' ) ) { 133 | /** 134 | * Returns media URI with optional appended path/file. 135 | * 136 | * @since 1.0.0 137 | */ 138 | function media_uri( string $append = '' ): string 139 | { 140 | return media_url( $append ); 141 | } 142 | } 143 | --------------------------------------------------------------------------------