From 6523d9f6177351147109dc4b3b1a5cf50cb93b51 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Tue, 9 Nov 2021 16:48:00 -0600 Subject: [PATCH 1/7] initial to establish notes --- src/HttpRequest.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/HttpRequest.php b/src/HttpRequest.php index 6b5ecd1b..eb3590f5 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -13,6 +13,7 @@ use Nyholm\Psr7\Factory\Psr17Factory as PsrFactory; use Nyholm\Psr7Server\ServerRequestCreator as PsrServerRequestCreator; +use JoshBruce\Site\FileSystem; use JoshBruce\Site\File; /** @@ -26,11 +27,24 @@ class HttpRequest public static function fromGlobals(): HttpRequest { + if ($fileSystem === null) { + $fileSystem = new FileSystem(); + } + return new HttpRequest(); } - private function __construct() - { + public static function for( + FileSystem $contentFolder, + array $serverGlobals + ): HttpRequest { + return new HttpRequest($contentFolder, $serverGlobals); + } + + private function __construct( + FileSystem $contentFolder, + array $serverGlobals + ) { } public function isMissingRequiredValues(): bool From bf89c153f9d8407df267b7e269d39fef6dcdac1b Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Tue, 9 Nov 2021 18:29:43 -0600 Subject: [PATCH 2/7] initial removal of $_SERVER references --- phpunit.xml | 1 + src/HttpRequest.php | 24 ++++++------- src/ServerGlobals.php | 28 ++++++++++------ src/SiteStatic/Generator.php | 25 ++++++-------- tests/GneratorTest.php | 3 ++ tests/Pest.php | 6 ++-- tests/ServerGlobalsTest.php | 22 ++++++++++++ tests/ServerTest.php | 65 ------------------------------------ tests/SitemapTest.php | 2 +- 9 files changed, 69 insertions(+), 107 deletions(-) create mode 100644 tests/GneratorTest.php create mode 100644 tests/ServerGlobalsTest.php delete mode 100644 tests/ServerTest.php diff --git a/phpunit.xml b/phpunit.xml index 1b7cef5f..8ce9ea37 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -28,4 +28,5 @@ + diff --git a/src/HttpRequest.php b/src/HttpRequest.php index eb3590f5..3c19e6c9 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -27,24 +27,20 @@ class HttpRequest public static function fromGlobals(): HttpRequest { - if ($fileSystem === null) { - $fileSystem = new FileSystem(); - } - +// if ($fileSystem === null) { +// $fileSystem = new FileSystem(); +// } +// return new HttpRequest(); } - public static function for( - FileSystem $contentFolder, - array $serverGlobals - ): HttpRequest { - return new HttpRequest($contentFolder, $serverGlobals); - } + // public static function for(FileSystem $contentFolder): HttpRequest + // { + // return new HttpRequest($contentFolder, $serverGlobals); + // } - private function __construct( - FileSystem $contentFolder, - array $serverGlobals - ) { + private function __construct() + { } public function isMissingRequiredValues(): bool diff --git a/src/ServerGlobals.php b/src/ServerGlobals.php index 0f11fd80..e411754c 100644 --- a/src/ServerGlobals.php +++ b/src/ServerGlobals.php @@ -6,6 +6,11 @@ class ServerGlobals { + /** + * @var array + */ + private array $globals = []; + public static function init(): ServerGlobals { return new ServerGlobals(); @@ -20,6 +25,15 @@ public function appEnvIsNot(string $value): bool return $this->appEnv() !== $value; } + private function appEnv(): string + { + if ($this->hasAppEnv()) { + $globals = $this->globals(); + return strval($globals['APP_ENV']); + } + return ''; + } + public function isMissingAppEnv(): bool { return ! $this->hasAppEnv(); @@ -39,15 +53,6 @@ public function appUrl(): string return ''; } - private function appEnv(): string - { - if ($this->hasAppEnv()) { - $globals = $this->globals(); - return strval($globals['APP_ENV']); - } - return ''; - } - private function hasAppEnv(): bool { return array_key_exists('APP_ENV', $this->globals()); @@ -63,6 +68,9 @@ private function hasAppUrl(): bool */ private function globals(): array { - return $_SERVER; + if (count($this->globals) === 0) { + $this->globals = $_SERVER; + } + return $this->globals; } } diff --git a/src/SiteStatic/Generator.php b/src/SiteStatic/Generator.php index 95687883..7bf21e87 100644 --- a/src/SiteStatic/Generator.php +++ b/src/SiteStatic/Generator.php @@ -15,6 +15,7 @@ use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Filesystem as LeagueFilesystem; +use JoshBruce\Site\ServerGlobals; use JoshBruce\Site\FileSystem; use JoshBruce\Site\HttpResponse; @@ -43,9 +44,7 @@ private function __construct( Dotenv::createImmutable($projectRoot)->load(); - if (isset($_SERVER['APP_ENV'])) { - $this->isNotTesting = $_SERVER['APP_ENV'] !== 'testing'; - } + $this->isNotTesting = ServerGlobals::init()->appEnv() !== 'test'; $this->contentRoot = FileSystem::contentRoot() . '/public'; @@ -83,34 +82,32 @@ private function compileContentFileFor(string $contentPath): void $parts = array_slice($parts, 0, -1); $requestUri = implode('/', $parts); - $_SERVER['REQUEST_URI'] = $requestUri; + $request = HttpRequest::fromGlobals()->withUri($requestUri); if (str_contains($destinationPath, '/error-404.html')) { - $_SERVER['REQUEST_URI'] = '/low/probability/of/ex/is/ting'; + $request = HttpRequest::fromGlobals()->withUri( + '/low/probability/of/ex/is/ting' + ); } elseif (str_contains($destinationPath, '/error-405.html')) { - $_SERVER['REQUEST_METHOD'] = 'DELETE'; + $request = HttpRequest::fromGlobals()->withMethod('DELETE'); } elseif (strlen($requestUri) === 0) { - $_SERVER['REQUEST_URI'] = '/'; + $request = HttpRequest::fromGlobals()->withUri('/'); } - // $_SERVER['REQUEST_URI'] = (strlen($requestUri) === 0) - // ? '/' - // : $requestUri; - - $html = HttpResponse::from(request: HttpRequest::fromGlobals())->body(); + $html = HttpResponse::from(request: $request)->body(); $this->leagueFileSystem()->write($destinationPath, $html); - // $this->sourceFileConvertedMessage($contentPath, $destinationPath); + $this->sourceFileConvertedMessage($contentPath, $destinationPath); } private function copyFileFor(string $path): void { $destinationPath = $this->fileDestinationPathFor($path); $this->leagueFileSystem()->copy($path, $destinationPath); - // $this->sourceFileCopiedMessage($path, $destinationPath); + $this->sourceFileCopiedMessage($path, $destinationPath); } private function contentDestinationPathFor(string $path): string diff --git a/tests/GneratorTest.php b/tests/GneratorTest.php new file mode 100644 index 00000000..d81d41a7 --- /dev/null +++ b/tests/GneratorTest.php @@ -0,0 +1,3 @@ +ignoreVCS(false)->files()->name('*.php')->in( + FileSystem::projectRoot() . '/src' + )->contains('$_SERVER'); + + foreach ($found as $f) { + $result = str_ends_with($f->getPathname(), 'ServerGlobals.php'); + if (! $result){ + var_dump($f->getPathname()); + } + $this->assertTrue($result); + } +})->group('server-globals', 'focus'); diff --git a/tests/ServerTest.php b/tests/ServerTest.php deleted file mode 100644 index d18626be..00000000 --- a/tests/ServerTest.php +++ /dev/null @@ -1,65 +0,0 @@ -projectRoot = implode('/', array_slice(explode('/', __DIR__), 0, -1)); -// -// serverGlobals(); -// -// $this->contentRoot = $this->projectRoot . $_SERVER['CONTENT_FOLDER']; -// }); -// -// it('has expected project roo', function() { -// expect( -// Server::projectRoot() -// )->toBeString()->toBe( -// $this->projectRoot -// ); -// })->group('server'); -// -// it('has expected file name and content root', function() { -// expect( -// Server::init(serverGlobals(), $this->projectRoot)->requestFileName() -// )->toBe( -// '' -// ); -// -// expect( -// Server::init(serverGlobals(), $this->projectRoot)->contentRoot() -// )->toBe( -// __DIR__ . '/test-content/content' -// ); -// })->group('server'); -// -// it('limits request methods', function() { -// expect( -// Server::init(serverGlobals(), $this->projectRoot) -// ->isRequestingUnsupportedMethod() -// )->toBeFalse(); -// -// $serverGlobals = serverGlobals(); -// $serverGlobals['REQUEST_METHOD'] = 'INVALID'; -// -// expect( -// Server::init($serverGlobals, $this->projectRoot) -// ->isRequestingUnsupportedMethod() -// )->toBeTrue(); -// })->group('server'); -// -// it('has required variables', function() { -// expect( -// Server::init(serverGlobals(), $this->projectRoot) -// ->isMissingRequiredValues() -// )->toBeFalse(); -// -// $serverGlobals = serverGlobals(); -// unset($serverGlobals['CONTENT_UP']); -// -// expect( -// Server::init($serverGlobals, $this->projectRoot) -// ->isMissingRequiredValues() -// )->toBeTrue(); -// })->group('server'); diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php index edadd0b3..752cfc9c 100644 --- a/tests/SitemapTest.php +++ b/tests/SitemapTest.php @@ -37,4 +37,4 @@ https://joshbruce.comhttps://joshbruce.com/design-your-lifehttps://joshbruce.com/design-your-life/motivatorshttps://joshbruce.com/finances/budgetinghttps://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210301https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210315https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210401https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210415https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210501https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210515https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210601https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210615https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210701https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210715https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210801https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210815https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210901https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210915https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211001https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211015https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211101https://joshbruce.com/finances/building-wealth-paycheck-to-paycheckhttps://joshbruce.com/financeshttps://joshbruce.com/finances/investment-policyhttps://joshbruce.com/health-and-wellnesshttps://joshbruce.com/legalhttps://joshbruce.com/software-developmenthttps://joshbruce.com/software-development/why-dont-you-usehttps://joshbruce.com/web-development/2021-site-in-depthhttps://joshbruce.com/web-developmenthttps://joshbruce.com/web-development/modern-web-developmenthttps://joshbruce.com/web-development/my-history-on-the-webhttps://joshbruce.com/web-development/on-constraintshttps://joshbruce.com/web-development/on-constraints/internet-bandwidthhttps://joshbruce.com/web-development/refactoring-re-engineering-and-rebuildinghttps://joshbruce.com/web-development/site-statshttps://joshbruce.com/web-development/static-dynamic-and-interactive xml ); -})->group('sitemap', 'focus'); +})->group('sitemap'); From 7895c3c4d06d7bf62a83e32d45c2c20dd5d66bcc Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Tue, 9 Nov 2021 20:08:44 -0600 Subject: [PATCH 3/7] able to send custom server globals to request next will want to use custom file system --- phpunit.xml | 4 +- src/HttpRequest.php | 127 ++++++++++++++++++----------------- src/HttpResponse.php | 60 +++++++++-------- src/ServerGlobals.php | 75 ++++++++++++++------- tests/HttpTest.php | 128 ++++++++++++++---------------------- tests/Pest.php | 28 -------- tests/ServerGlobalsTest.php | 26 +++++++- tests/TestFileSystem.php | 12 ++++ 8 files changed, 236 insertions(+), 224 deletions(-) create mode 100644 tests/TestFileSystem.php diff --git a/phpunit.xml b/phpunit.xml index 8ce9ea37..0558506c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -27,6 +27,8 @@ + + - + diff --git a/src/HttpRequest.php b/src/HttpRequest.php index 3c19e6c9..56ef8eed 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -13,7 +13,9 @@ use Nyholm\Psr7\Factory\Psr17Factory as PsrFactory; use Nyholm\Psr7Server\ServerRequestCreator as PsrServerRequestCreator; +use JoshBruce\Site\ServerGlobals; use JoshBruce\Site\FileSystem; + use JoshBruce\Site\File; /** @@ -25,31 +27,32 @@ class HttpRequest private string $localPath = ''; - public static function fromGlobals(): HttpRequest - { -// if ($fileSystem === null) { -// $fileSystem = new FileSystem(); -// } + public static function with( + ServerGlobals $serverGlobals, + FileSystem $in + ): HttpRequest { + return new HttpRequest($serverGlobals, $in); + } + +// // public static function fromGlobals(): HttpRequest +// // { +// // // if ($fileSystem === null) { +// // // $fileSystem = new FileSystem(); +// // // } +// // // +// // return new HttpRequest(); +// // } // - return new HttpRequest(); - } - - // public static function for(FileSystem $contentFolder): HttpRequest - // { - // return new HttpRequest($contentFolder, $serverGlobals); - // } - - private function __construct() - { - } - - public function isMissingRequiredValues(): bool - { - if ($this->serverGlobals()->isMissingAppEnv()) { - return true; - } - - if ($this->serverGlobals()->appEnvIsNot('production')) { +// // public static function for(FileSystem $contentFolder): HttpRequest +// // { +// // return new HttpRequest($contentFolder, $serverGlobals); +// // } +// + private function __construct( + private ServerGlobals $serverGlobals, + private FileSystem $fileSystem + ) { + if ($this->serverGlobals()->appEnv() !== 'production') { // use Whoops! for error display $errorHandler = new ErrorHandler(); $errorHandler->pushHandler( @@ -57,27 +60,43 @@ public function isMissingRequiredValues(): bool ); $errorHandler->register(); } - return false; + } + + public function isMissingRequiredValues(): bool + { + return $this->serverGlobals()->isMissingRequiredValues(); } public function isUnsupportedMethod(): bool { - return ! $this->isSupportedMethod(); + $requestMethod = strtoupper($this->psrRequest()->getMethod()); + $isSupported = in_array($requestMethod, $this->supportedMethods()); + + return ! $isSupported; } public function isNotFound(): bool { - return ! $this->isFound(); + $isFound = file_exists( + $this->localPath()) and + is_file($this->localPath() + ); + return ! $isFound; } - public function isFile(): bool + public function localFile(): File { - return str_contains($this->possibleFileName(), '.'); + return File::at(localPath: $this->localPath()); } +// +// public function isFile(): bool +// { +// return str_contains($this->possibleFileName(), '.'); +// } public function isSitemap(): bool { - return $this->isFile() and $this->possibleFileName() === 'sitemap.xml'; + return $this->possibleFileName() === 'sitemap.xml'; } public function isNotSitemap(): bool @@ -85,27 +104,13 @@ public function isNotSitemap(): bool return ! $this->isSitemap(); } - public function localFile(): File - { - return File::at(localPath: $this->localPath()); - } - - private function isFound(): bool - { - return file_exists($this->localPath()) and is_file($this->localPath()); - } - private function localPath(): string { if (empty($this->localPath)) { $possibleFileName = $this->possibleFileName(); - $relativePath = $this->uriPath(); + $relativePath = $this->psrPath(); if (empty($possibleFileName)) { - $relativePath = $this->uriPath() . '/content.md'; - - // } elseif (str_contains($relativePath, '.xml')) { - // $relativePath = str_replace('.xml', '.md', $relativePath); - + $relativePath = $this->psrPath() . '/content.md'; } if (! str_starts_with($relativePath, '/')) { @@ -121,7 +126,7 @@ private function localPath(): string private function possibleFileName(): string { - $parts = explode('/', $this->uriPath()); + $parts = explode('/', $this->psrPath()); $lastPart = array_slice($parts, -1); $possibleFileName = array_shift($lastPart); if ( @@ -141,15 +146,9 @@ private function supportedMethods(): array return ['GET']; } - private function isSupportedMethod(): bool - { - $requestMethod = strtoupper($this->psrRequest()->getMethod()); - return in_array($requestMethod, $this->supportedMethods()); - } - private function serverGlobals(): ServerGlobals { - return ServerGlobals::init(); + return $this->serverGlobals; } private function psrRequest(): RequestInterface @@ -168,17 +167,17 @@ private function psrRequest(): RequestInterface return $this->psrRequest; } - private function uriPath(): string + private function psrPath(): string { - $uriPath = $this->uri()->getPath(); - if ($uriPath === '/') { - $uriPath = ''; + $psrPath = $this->psrRequest()->getUri()->getPath(); + if ($psrPath === '/') { + $psrPath = ''; } - return $uriPath; - } - - private function uri(): UriInterface - { - return $this->psrRequest()->getUri(); + return $psrPath; } +// +// private function uri(): UriInterface +// { +// return $this->psrRequest()->getUri(); +// } } diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 7f9e7d36..0134f175 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -38,13 +38,13 @@ private function __construct(private HttpRequest $request) public function statusCode(): int { - if ($this->request->isMissingRequiredValues()) { + if ($this->request()->isMissingRequiredValues()) { return 500; - } elseif ($this->request->isUnsupportedMethod()) { + } elseif ($this->request()->isUnsupportedMethod()) { return 405; - } elseif ($this->request->isNotFound()) { + } elseif ($this->request()->isNotFound()) { return 404; } @@ -58,23 +58,22 @@ public function headers(): array { $headers = []; if ($this->statusCode() === 200) { - $headers['Content-Type'] = $this->request->localFile()->mimeType(); + $headers['Content-Type'] = $this->request()->localFile()->mimeType(); } elseif ($this->statusCode() === 404) { $headers['Content-Type'] = 'text/html'; } - return $headers; } public function body(): string { - $localFile = $this->request->localFile(); + $localFile = $this->request()->localFile(); if ( $this->statusCode() === 200 and $localFile->isNotMarkdown() and - $this->request->isNotSitemap() + $this->request()->isNotSitemap() ) { return $localFile->path(); @@ -96,12 +95,10 @@ public function body(): string $template = $markdown->frontMatter()->template(); $pageTitle = $markdown->pageTitle(); $html = $markdown->html(); - } - if ($this->request->isSitemap()) { + if ($this->request()->isSitemap()) { return Sitemap::create(); - } return Document::create( @@ -160,25 +157,30 @@ public function cssElement(): Element ->props('rel stylesheet', "href {$cssPath}?v={$query}"); } - public function psrResponse(): ResponseInterface +// public function psrResponse(): ResponseInterface +// { +// if (! isset($this->psrResponse)) { +// $psr17Factory = new PsrFactory(); +// $body = $this->body(); +// $stream = $psr17Factory->createStream($body); +// if ( +// $this->request()->isFile() and +// $this->request()->isNotSitemap() +// ) { +// $stream = $psr17Factory->createStreamFromFile($body); +// } +// +// $this->psrResponse = new PsrResponse( +// $this->statusCode(), +// $this->headers(), +// $stream +// ); +// } +// return $this->psrResponse; +// } +// + private function request(): HttpRequest { - if (! isset($this->psrResponse)) { - $psr17Factory = new PsrFactory(); - $body = $this->body(); - $stream = $psr17Factory->createStream($body); - if ( - $this->request->isFile() and - $this->request->isNotSitemap() - ) { - $stream = $psr17Factory->createStreamFromFile($body); - } - - $this->psrResponse = new PsrResponse( - $this->statusCode(), - $this->headers(), - $stream - ); - } - return $this->psrResponse; + return $this->request; } } diff --git a/src/ServerGlobals.php b/src/ServerGlobals.php index e411754c..97367fd1 100644 --- a/src/ServerGlobals.php +++ b/src/ServerGlobals.php @@ -18,51 +18,82 @@ public static function init(): ServerGlobals private function __construct() { + $this->globals = $_SERVER; } - public function appEnvIsNot(string $value): bool + public function withRequestUri(string $method): ServerGlobals { - return $this->appEnv() !== $value; + $this->globals = []; + + $_SERVER['REQUEST_URI'] = $method; + return $this; } - private function appEnv(): string + public function requestUri(): string { - if ($this->hasAppEnv()) { - $globals = $this->globals(); - return strval($globals['APP_ENV']); - } - return ''; + $globals = $this->globals(); + return $globals['REQUEST_URI']; } - public function isMissingAppEnv(): bool + public function withRequestMethod(string $method): ServerGlobals { - return ! $this->hasAppEnv(); + $this->globals = []; + + $_SERVER['REQUEST_METHOD'] = $method; + return $this; } - public function isMissingAppUrl(): bool + public function requestMethod(): string { - return ! $this->hasAppUrl(); + $globals = $this->globals(); + return $globals['REQUEST_METHOD']; } - public function appUrl(): string + public function isMissingRequiredValues(): bool { - if ($this->hasAppUrl()) { - $globals = $this->globals(); - return strval($globals['APP_URL']); - } - return ''; + return ! $this->hasRequiredValues(); } - private function hasAppEnv(): bool + private function hasRequiredValues(): bool { - return array_key_exists('APP_ENV', $this->globals()); + $globals = $this->globals(); + return array_key_exists('APP_ENV', $globals) and + array_key_exists('APP_URL', $globals); } - private function hasAppUrl(): bool +// public function appEnvIsNot(string $value): bool +// { +// return $this->appEnv() !== $value; +// } +// + public function appEnv(): string { - return array_key_exists('APP_URL', $this->globals()); + if ($this->hasRequiredValues()) { + $globals = $this->globals(); + return strval($globals['APP_ENV']); + } + return ''; } +// public function isMissingAppEnv(): bool +// { +// return ! $this->hasAppEnv(); +// } +// +// public function isMissingAppUrl(): bool +// { +// return ! $this->hasAppUrl(); +// } +// +// public function appUrl(): string +// { +// if ($this->hasAppUrl()) { +// $globals = $this->globals(); +// return strval($globals['APP_URL']); +// } +// return ''; +// } + /** * @return array */ diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 8d8d375a..188cbac5 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -5,43 +5,52 @@ use JoshBruce\Site\HttpResponse; use JoshBruce\Site\HttpRequest; -test('expected headers', function() { - serverGlobals(); +use JoshBruce\Site\ServerGlobals; + +use JoshBruce\Site\Tests\TestFileSystem; +test('expected headers', function() { expect( HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init(), + new TestFileSystem() + ) )->headers() )->toBe( ['Content-Type' => 'text/html'] ); - serverGlobals('/assets/css/main.min.css'); - expect( HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init() + ->withRequestUri('/assets/css/main.min.css'), + new TestFileSystem() + ) )->headers() )->toBe( ['Content-Type' => 'text/css'] ); -})->group('response'); +})->group('response', 'request', 'server-globals'); test('expected titles', function() { - serverGlobals(); - $body = HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init()->withRequestUri('/'), + new TestFileSystem() + ) )->body(); expect( str_contains($body, "Josh Bruce's personal site") )->toBeTrue(); - serverGlobals('/finances'); - $body = HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init()->withRequestUri('/finances'), + new TestFileSystem() + ) )->body(); expect( @@ -51,10 +60,11 @@ ) )->toBeTrue(); - serverGlobals('/something-missing'); - $body = HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init()->withRequestUri('/something/invalid'), + new TestFileSystem() + ) )->body(); expect( @@ -63,94 +73,54 @@ "Page not found" ) )->toBeTrue(); -})->group('response'); +})->group('response', 'request', 'server-globals'); test('expected status codes', function() { - serverGlobals(); - expect( HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init()->withRequestUri('/'), + new TestFileSystem() + ) )->statusCode() )->toBeInt()->toBe( 200 ); - unset($_SERVER['APP_ENV']); - expect( HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init()->withRequestUri('/something/invalid'), + new TestFileSystem() + ) )->statusCode() )->toBeInt()->toBe( - 500 + 404 ); - serverGlobals(); - - $_SERVER['REQUEST_METHOD'] = 'post'; - expect( HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init()->withRequestMethod('post'), + new TestFileSystem() + ) )->statusCode() )->toBeInt()->toBe( 405 ); - serverGlobals('/not-valid'); + unset($_SERVER['APP_URL']); expect( HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init(), + new TestFileSystem() + ) )->statusCode() )->toBeInt()->toBe( - 404 + 500 ); -})->group('response'); -// -// test('can check request is valid', function() { -// serverGlobals(); -// -// expect( -// HttpRequest::init()->isMissingRequiredValues() -// )->toBeFalse(); -// -// expect( -// HttpRequest::init()->isUnsupportedMethod() -// )->toBeFalse(); -// -// expect( -// HttpRequest::init()->isNotFound() -// )->toBeFalse(); -// -// serverGlobals('/not-valid'); -// unset($_SERVER['APP_ENV']); -// $_SERVER['REQUEST_METHOD'] = 'post'; -// -// expect( -// HttpRequest::init()->isMissingRequiredValues() -// )->toBeTrue(); -// -// expect( -// HttpRequest::init()->isUnsupportedMethod() -// )->toBeTrue(); -// -// expect( -// HttpRequest::init()->isNotFound() -// )->toBeTrue(); -// })->group('request'); -// -// it('uses server globals', function() { -// serverGlobals(); -// -// expect( -// ServerGlobals::init()->hasAppEnv() -// )->toBeTrue(); -// -// unset($_SERVER['APP_ENV']); -// -// expect( -// ServerGlobals::init()->hasAppEnv() -// )->toBeFalse(); -// })->group('globals'); + + $_SERVER['APP_URL'] = 'http://jb-site.test'; +})->group('response', 'request', 'server-globals'); diff --git a/tests/Pest.php b/tests/Pest.php index 3cb4842e..19377289 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,8 +11,6 @@ | */ -// uses(Tests\TestCase::class)->in('Feature'); - /* |-------------------------------------------------------------------------- | Functions @@ -23,29 +21,3 @@ | global functions to help you to reduce the number of lines of code in your test files. | */ - -use JoshBruce\Site\Environment; -use JoshBruce\Site\Server; - -function environment(string $requestUri = '/'): Environment -{ - return Environment::init(server($requestUri)); -} - -function server(string $requestUri = '/'): Server -{ - return Server::init(serverGlobals($requestUri)); -} - -function serverGlobals(string $requestUri = '/'): array -{ - // $_SERVER['APP_ENV'] = 'test'; - // $_SERVER['CONTENT_UP'] = 0; - // $_SERVER['CONTENT_FOLDER'] = '/tests/test-content/content'; - // $_SERVER['REQUEST_SCHEME'] = 'http'; - // $_SERVER['HTTP_HOST'] = 'testing.com'; - // $_SERVER['REQUEST_URI'] = $requestUri; - // $_SERVER['REQUEST_METHOD'] = 'GET'; - - return $_SERVER; -} diff --git a/tests/ServerGlobalsTest.php b/tests/ServerGlobalsTest.php index a91822cf..744a8e91 100644 --- a/tests/ServerGlobalsTest.php +++ b/tests/ServerGlobalsTest.php @@ -6,6 +6,30 @@ use Symfony\Component\Finder\Finder; +it('has required values', function() { + expect( + ServerGlobals::init()->withRequestUri('/something')->requestUri() + )->toBe( + '/something' + ); + + expect( + ServerGlobals::init()->withRequestMethod('GET')->requestMethod() + )->toBe( + 'GET' + ); + + expect( + ServerGlobals::init()->withRequestMethod('POST')->requestMethod() + )->toBe( + 'POST' + ); + + expect( + ServerGlobals::init()->isMissingRequiredValues() + )->toBeFalse(); +})->group('server-globals'); + test('only ServerGlobals references $_SERVER', function() { $finder = new Finder(); $found = $finder->ignoreVCS(false)->files()->name('*.php')->in( @@ -19,4 +43,4 @@ } $this->assertTrue($result); } -})->group('server-globals', 'focus'); +})->group('server-globals'); diff --git a/tests/TestFileSystem.php b/tests/TestFileSystem.php new file mode 100644 index 00000000..95cdd469 --- /dev/null +++ b/tests/TestFileSystem.php @@ -0,0 +1,12 @@ + Date: Tue, 9 Nov 2021 20:20:54 -0600 Subject: [PATCH 4/7] FileSystem as injectable --- site-dynamic-php/index.php | 5 ++++- src/FileSystem.php | 13 +++++++++-- src/HttpRequest.php | 16 ++++++-------- src/HttpResponse.php | 44 +++++++++++++++++++------------------- src/ServerGlobals.php | 4 ++-- tests/HttpTest.php | 18 ++++++++-------- 6 files changed, 55 insertions(+), 45 deletions(-) diff --git a/site-dynamic-php/index.php b/site-dynamic-php/index.php index 5bf35bf0..fbe2b1b9 100644 --- a/site-dynamic-php/index.php +++ b/site-dynamic-php/index.php @@ -14,7 +14,10 @@ JoshBruce\Site\SiteDynamic\Emitter::emit( response:JoshBruce\Site\HttpResponse::from( - request: JoshBruce\Site\HttpRequest::fromGlobals() + request: JoshBruce\Site\HttpRequest::with( + JoshBruce\Site\ServerGlobals::init(), + JoshBruce\Site\FileSystem::init() + ) ) ); exit; diff --git a/src/FileSystem.php b/src/FileSystem.php index e82af58d..8f2d4c7a 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -10,14 +10,19 @@ class FileSystem { + public static function init(): static + { + return new static(static::projectRoot()); + } + public static function publicRoot(): string { - return FileSystem::contentRoot() . '/public'; + return static::contentRoot() . '/public'; } public static function contentRoot(): string { - $parts = explode('/', self::projectRoot()); + $parts = explode('/', static::projectRoot()); $parts[] = 'content'; $base = implode('/', $parts); if (str_ends_with($base, '/')) { @@ -63,4 +68,8 @@ private static function relativePath(string $path): string { return str_replace(self::contentRoot(), '', $path); } + + final private function __construct(protected string $projectRoot) + { + } } diff --git a/src/HttpRequest.php b/src/HttpRequest.php index 56ef8eed..354c806a 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -77,10 +77,8 @@ public function isUnsupportedMethod(): bool public function isNotFound(): bool { - $isFound = file_exists( - $this->localPath()) and - is_file($this->localPath() - ); + $isFound = file_exists($this->localPath()) and + is_file($this->localPath()); return ! $isFound; } @@ -88,11 +86,11 @@ public function localFile(): File { return File::at(localPath: $this->localPath()); } -// -// public function isFile(): bool -// { -// return str_contains($this->possibleFileName(), '.'); -// } + + public function isFile(): bool + { + return str_contains($this->possibleFileName(), '.'); + } public function isSitemap(): bool { diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 0134f175..f5d3ac76 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -157,28 +157,28 @@ public function cssElement(): Element ->props('rel stylesheet', "href {$cssPath}?v={$query}"); } -// public function psrResponse(): ResponseInterface -// { -// if (! isset($this->psrResponse)) { -// $psr17Factory = new PsrFactory(); -// $body = $this->body(); -// $stream = $psr17Factory->createStream($body); -// if ( -// $this->request()->isFile() and -// $this->request()->isNotSitemap() -// ) { -// $stream = $psr17Factory->createStreamFromFile($body); -// } -// -// $this->psrResponse = new PsrResponse( -// $this->statusCode(), -// $this->headers(), -// $stream -// ); -// } -// return $this->psrResponse; -// } -// + public function psrResponse(): ResponseInterface + { + if (! isset($this->psrResponse)) { + $psr17Factory = new PsrFactory(); + $body = $this->body(); + $stream = $psr17Factory->createStream($body); + if ( + $this->request()->isFile() and + $this->request()->isNotSitemap() + ) { + $stream = $psr17Factory->createStreamFromFile($body); + } + + $this->psrResponse = new PsrResponse( + $this->statusCode(), + $this->headers(), + $stream + ); + } + return $this->psrResponse; + } + private function request(): HttpRequest { return $this->request; diff --git a/src/ServerGlobals.php b/src/ServerGlobals.php index 97367fd1..ec1e1600 100644 --- a/src/ServerGlobals.php +++ b/src/ServerGlobals.php @@ -32,7 +32,7 @@ public function withRequestUri(string $method): ServerGlobals public function requestUri(): string { $globals = $this->globals(); - return $globals['REQUEST_URI']; + return strval($globals['REQUEST_URI']); } public function withRequestMethod(string $method): ServerGlobals @@ -46,7 +46,7 @@ public function withRequestMethod(string $method): ServerGlobals public function requestMethod(): string { $globals = $this->globals(); - return $globals['REQUEST_METHOD']; + return strval($globals['REQUEST_METHOD']); } public function isMissingRequiredValues(): bool diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 188cbac5..338a01e2 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -14,7 +14,7 @@ HttpResponse::from( request: HttpRequest::with( ServerGlobals::init(), - new TestFileSystem() + TestFileSystem::init() ) )->headers() )->toBe( @@ -26,7 +26,7 @@ request: HttpRequest::with( ServerGlobals::init() ->withRequestUri('/assets/css/main.min.css'), - new TestFileSystem() + TestFileSystem::init() ) )->headers() )->toBe( @@ -38,7 +38,7 @@ $body = HttpResponse::from( request: HttpRequest::with( ServerGlobals::init()->withRequestUri('/'), - new TestFileSystem() + TestFileSystem::init() ) )->body(); @@ -49,7 +49,7 @@ $body = HttpResponse::from( request: HttpRequest::with( ServerGlobals::init()->withRequestUri('/finances'), - new TestFileSystem() + TestFileSystem::init() ) )->body(); @@ -63,7 +63,7 @@ $body = HttpResponse::from( request: HttpRequest::with( ServerGlobals::init()->withRequestUri('/something/invalid'), - new TestFileSystem() + TestFileSystem::init() ) )->body(); @@ -80,7 +80,7 @@ HttpResponse::from( request: HttpRequest::with( ServerGlobals::init()->withRequestUri('/'), - new TestFileSystem() + TestFileSystem::init() ) )->statusCode() )->toBeInt()->toBe( @@ -91,7 +91,7 @@ HttpResponse::from( request: HttpRequest::with( ServerGlobals::init()->withRequestUri('/something/invalid'), - new TestFileSystem() + TestFileSystem::init() ) )->statusCode() )->toBeInt()->toBe( @@ -102,7 +102,7 @@ HttpResponse::from( request: HttpRequest::with( ServerGlobals::init()->withRequestMethod('post'), - new TestFileSystem() + TestFileSystem::init() ) )->statusCode() )->toBeInt()->toBe( @@ -115,7 +115,7 @@ HttpResponse::from( request: HttpRequest::with( ServerGlobals::init(), - new TestFileSystem() + TestFileSystem::init() ) )->statusCode() )->toBeInt()->toBe( From 5cf5014559ee5988965fe62ea875d789ad16c209 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Wed, 10 Nov 2021 11:38:24 -0600 Subject: [PATCH 5/7] FileSystem made testable --- src/FileSystem.php | 77 ++++--- tests/FileSystemTest.php | 212 +++--------------- tests/TestFileSystem.php | 7 +- tests/test-content/content/public/.gitignore | 1 + tests/test-content/content/public/content.md | 0 .../_content.md | 0 .../content/public/published-sub/content.md | 0 7 files changed, 91 insertions(+), 206 deletions(-) create mode 100644 tests/test-content/content/public/.gitignore create mode 100644 tests/test-content/content/public/content.md create mode 100644 tests/test-content/content/public/published-folder-with-draft-content/_content.md create mode 100644 tests/test-content/content/public/published-sub/content.md diff --git a/src/FileSystem.php b/src/FileSystem.php index 8f2d4c7a..7855d15a 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -15,12 +15,27 @@ public static function init(): static return new static(static::projectRoot()); } - public static function publicRoot(): string + public static function projectRoot(): string + { + $dir = __DIR__; + $parts = explode('/', $dir); + $parts = array_slice($parts, 0, -1); + return implode('/', $parts); + } + + final private function __construct(protected string $projectRoot) + { + } + + public function hasRequiredFolders(): bool { - return static::contentRoot() . '/public'; + return file_exists($this->contentRoot()) and + file_exists($this->publicRoot()) and + is_dir($this->contentRoot()) and + is_dir($this->publicRoot()); } - public static function contentRoot(): string + public function contentRoot(): string { $parts = explode('/', static::projectRoot()); $parts[] = 'content'; @@ -31,45 +46,57 @@ public static function contentRoot(): string return $base; } - public static function projectRoot(): string + public function publicRoot(): string { - $dir = __DIR__; - $parts = explode('/', $dir); - $parts = array_slice($parts, 0, -1); - return implode('/', $parts); + return $this->contentRoot() . '/public'; } - public static function finder(): Finder + public function publishedContentFinder(): Finder { - $finder = new Finder(); - return $finder->ignoreVCS(false) - ->ignoreUnreadableDirs() - ->ignoreDotFiles(false) - ->ignoreVCSIgnored(true) - ->notName('.gitignore') - ->files() - ->filter(fn($f) => self::isPublished($f)) - ->in(self::publicRoot()); + return $this->finder()->in($this->publicRoot())->name('content.md') + ->filter(fn($f) => $this->isPublished($f)); + } + + private function relativePath(string $path): string + { + return str_replace($this->contentRoot(), '', $path); } - private static function isPublished(SplFileInfo $finderFile): bool + private function isPublished(SplFileInfo $finderFile): bool { - return ! self::isDraft($finderFile); + return ! $this->isDraft($finderFile); } - private static function isDraft(SplFileInfo $finderFile): bool + private function isDraft(SplFileInfo $finderFile): bool { $filePath = (string) $finderFile; - $relativePath = self::relativePath($filePath); + $relativePath = $this->relativePath($filePath); return str_contains($relativePath, '_'); } - private static function relativePath(string $path): string + private function projectRootFinder(): Finder { - return str_replace(self::contentRoot(), '', $path); + return $this->finder()->in(static::projectRoot()); } - final private function __construct(protected string $projectRoot) + private function contentRootFinder(): Finder + { + return $this->finder()->in($this->contentRoot()); + } + + private function publicRootFinder(): Finder { + return $this->finder()->in($this->publicRoot()); + } + + private function finder(): Finder + { + $finder = new Finder(); + return $finder->ignoreVCS(false) + ->ignoreUnreadableDirs() + ->ignoreDotFiles(false) + ->ignoreVCSIgnored(true) + ->notName('.gitignore') + ->files(); } } diff --git a/tests/FileSystemTest.php b/tests/FileSystemTest.php index 3cb762b6..392b5cce 100644 --- a/tests/FileSystemTest.php +++ b/tests/FileSystemTest.php @@ -1,181 +1,33 @@ projectRoot = implode('/', array_slice(explode('/', __DIR__), 0, -1)); -// -// serverGlobals(); -// -// $this->contentRoot = $this->projectRoot . '/content'; -// }); -// -// test('content folder does exist', function() { -// expect( -// FileSystem::contentFolderIsMissing() -// )->toBeFalse(); -// })->group('filesystem'); -// -// it('can initialize folders', function() { -// expect( -// FileSystem::public()->path() -// )->toBeString()->toBe( -// "{$this->contentRoot}/public" -// ); -// -// expect( -// FileSystem::navigation()->path() -// )->toBeString()->toBe( -// "{$this->contentRoot}/navigation" -// ); -// })->group('filesystem'); -// -// // it('can return folder tree', function() { -// // $this->assertEquals( -// // FileSystem::init( -// // $this->contentRoot, -// // 'public', -// // 'finances', -// // 'investment-policy' -// // )->folderStack(), -// // [ -// // FileSystem::init( -// // $this->contentRoot, -// // 'public', -// // 'finances', -// // 'investment-policy' -// // ), -// // FileSystem::init( -// // $this->contentRoot, -// // 'public', -// // 'finances' -// // ), -// // FileSystem::init( -// // $this->contentRoot, -// // 'public' -// // ), -// // FileSystem::init( -// // $this->contentRoot -// // ) -// // ] -// // ); -// // -// // $this->assertEquals( -// // FileSystem::init($this->contentRoot) -// // ->with(folderPath: '/sub-folder') -// // ->folderStack(fileName: 'content.md'), -// // [ -// // FileSystem::init($this->contentRoot)->with( -// // folderPath: '/sub-folder', -// // fileName: 'content.md' -// // ), -// // FileSystem::init($this->contentRoot) -// // ->with(folderPath: '', fileName: 'content.md') -// // ] -// // ); -// // })->group('filesystem', 'focus'); -// // -// // it('can determine if path is content root', function() { -// // expect( -// // FileSystem::init($this->contentRoot)->isRoot() -// // )->toBeTrue(); -// // -// // expect( -// // FileSystem::init($this->contentRoot, '/not-there')->isRoot() -// // )->toBeFalse(); -// // -// // expect( -// // FileSystem::init($this->contentRoot)->isNotRoot() -// // )->toBeFalse(); -// // -// // expect( -// // FileSystem::init($this->contentRoot, '/not-there')->isNotRoot() -// // )->toBeTrue(); -// // })->group('filesystem'); -// -// // it('can determine if path exists', function() { -// // expect( -// // FileSystem::init($this->contentRoot)->found() -// // )->toBeTrue(); -// // -// // expect( -// // FileSystem::init($this->contentRoot . '/not-there')->found() -// // )->toBeFalse(); -// // -// // expect( -// // FileSystem::init($this->contentRoot)->notFound() -// // )->toBeFalse(); -// // -// // expect( -// // FileSystem::init($this->contentRoot . '/not-there')->notFound() -// // )->toBeTrue(); -// // })->group('filesystem'); -// // -// // it('can detect whether the root folder exists', function() { -// // expect( -// // FileSystem::init($this->contentRoot)->rootFolderIsMissing() -// // )->toBeFalse(); -// // -// // expect( -// // FileSystem::init($this->contentRoot . '/not-there')->rootFolderIsMissing() -// // )->toBeTrue(); -// // })->group('filesystem'); -// // -// // it('has correct file path', function() { -// // expect( -// // FileSystem::init($this->contentRoot, 'css', 'main.min.css')->path() -// // )->toBe( -// // "{$this->contentRoot}/css/main.min.css" -// // ); -// // -// // expect( -// // FileSystem::init($this->contentRoot)->navigation('main.md')->path() -// // )->toBe( -// // "{$this->contentRoot}/navigation/main.md" -// // ); -// // -// // expect( -// // FileSystem::init($this->contentRoot)->messages('original.md')->path() -// // )->toBe( -// // "{$this->contentRoot}/messages/original.md" -// // ); -// // -// // expect( -// // FileSystem::init($this->contentRoot)->messages()->path() -// // )->toBe( -// // "{$this->contentRoot}/messages" -// // ); -// // -// // expect( -// // FileSystem::init($this->contentRoot, 'public', 'legal', 'content.md') -// // ->path(false) -// // )->toBe( -// // '/public/legal/content.md' -// // ); -// // })->group('filesystem'); -// // -// // it('has correct mimetypes', function() { -// // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/ -// // // MIME_types#textjavascript -// // expect( -// // FileSystem::init($this->contentRoot, 'gulpfile.js')->mimetype() -// // )->toBe( -// // 'text/javascript' -// // ); -// // -// // expect( -// // FileSystem::init($this->contentRoot, 'css', 'main.min.css')->mimetype() -// // )->toBe( -// // 'text/css' -// // ); -// // -// // expect( -// // FileSystem::init($this->contentRoot, 'public', 'content.md')->mimetype() -// // )->toBe( -// // 'text/html' -// // ); -// // })->group('filesystem'); + +use JoshBruce\Site\Tests\TestFileSystem; + +beforeEach(function() { + $this->projectRoot = TestFileSystem::projectRoot(); +}); + +it('can get published content', function() { + expect( + count( + TestFileSystem::init()->publishedContentFinder() + ) + )->toBeInt()->toBe(2); +})->group('filesystem'); + +it('has required folders', function() { + expect( + TestFileSystem::init()->hasRequiredFolders() + )->toBeTrue(); + + expect( + TestFileSystem::init()->contentRoot() + )->toBe( + $this->projectRoot . '/content' + ); + + expect( + TestFileSystem::init()->publicRoot() + )->toBe( + $this->projectRoot . '/content/public' + ); +})->group('filesystem'); diff --git a/tests/TestFileSystem.php b/tests/TestFileSystem.php index 95cdd469..1bf6effc 100644 --- a/tests/TestFileSystem.php +++ b/tests/TestFileSystem.php @@ -8,5 +8,10 @@ class TestFileSystem extends FileSystem { - + public static function projectRoot(): string + { + // TODO: We're not actually using the instance yet - make static functions + // into instance methods + return __DIR__ . '/test-content'; + } } diff --git a/tests/test-content/content/public/.gitignore b/tests/test-content/content/public/.gitignore new file mode 100644 index 00000000..e43b0f98 --- /dev/null +++ b/tests/test-content/content/public/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/tests/test-content/content/public/content.md b/tests/test-content/content/public/content.md new file mode 100644 index 00000000..e69de29b diff --git a/tests/test-content/content/public/published-folder-with-draft-content/_content.md b/tests/test-content/content/public/published-folder-with-draft-content/_content.md new file mode 100644 index 00000000..e69de29b diff --git a/tests/test-content/content/public/published-sub/content.md b/tests/test-content/content/public/published-sub/content.md new file mode 100644 index 00000000..e69de29b From 32b08b10b8552d946e13ffb6c380859c542c1697 Mon Sep 17 00:00:00 2001 From: Josh Bruce Date: Wed, 10 Nov 2021 16:46:30 -0600 Subject: [PATCH 6/7] increased testability --- src/Content/Markdown.php | 38 +- src/Documents/Sitemap.php | 9 +- src/File.php | 26 +- src/FileSystem.php | 19 +- src/FileSystemInterface.php | 22 ++ src/HttpRequest.php | 35 +- src/HttpResponse.php | 20 +- src/PageComponents/LogList.php | 9 +- src/PageComponents/Navigation.php | 14 +- src/PageComponents/OriginalContentNotice.php | 15 +- src/ServerGlobals.php | 4 +- src/SiteStatic/Generator.php | 24 +- tests/FileTest.php | 26 +- tests/HttpTest.php | 12 +- tests/SitemapTest.php | 18 +- .../content/public/assets/css/main.css | 372 ++++++++++++++++++ .../content/public/assets/css/main.min.css | 2 + .../public/assets/css/main.min.css.map | 1 + .../favicons/android-chrome-192x192.png | Bin 0 -> 5553 bytes .../favicons/android-chrome-512x512.png | Bin 0 -> 15975 bytes .../assets/favicons/apple-touch-icon.png | Bin 0 -> 5015 bytes .../public/assets/favicons/favicon-16x16.png | Bin 0 -> 571 bytes .../public/assets/favicons/favicon-32x32.png | Bin 0 -> 922 bytes .../public/assets/favicons/favicon.ico | Bin 0 -> 15406 bytes .../public/assets/favicons/site.webmanifest | 1 + tests/test-content/content/public/content.md | 3 + .../test-content/content/public/error-404.md | 9 + .../content/public/published-sub/content.md | 3 + tests/test-content/content/public/sitemap.xml | 3 + 29 files changed, 566 insertions(+), 119 deletions(-) create mode 100644 src/FileSystemInterface.php create mode 100644 tests/test-content/content/public/assets/css/main.css create mode 100644 tests/test-content/content/public/assets/css/main.min.css create mode 100644 tests/test-content/content/public/assets/css/main.min.css.map create mode 100644 tests/test-content/content/public/assets/favicons/android-chrome-192x192.png create mode 100644 tests/test-content/content/public/assets/favicons/android-chrome-512x512.png create mode 100644 tests/test-content/content/public/assets/favicons/apple-touch-icon.png create mode 100644 tests/test-content/content/public/assets/favicons/favicon-16x16.png create mode 100644 tests/test-content/content/public/assets/favicons/favicon-32x32.png create mode 100644 tests/test-content/content/public/assets/favicons/favicon.ico create mode 100644 tests/test-content/content/public/assets/favicons/site.webmanifest create mode 100644 tests/test-content/content/public/error-404.md create mode 100644 tests/test-content/content/public/sitemap.xml diff --git a/src/Content/Markdown.php b/src/Content/Markdown.php index 49429a04..9b05d1a7 100644 --- a/src/Content/Markdown.php +++ b/src/Content/Markdown.php @@ -7,6 +7,7 @@ use Eightfold\Markdown\Markdown as MarkdownConverter; use JoshBruce\Site\File; +use JoshBruce\Site\FileSystemInterface; use JoshBruce\Site\PageComponents\Data; use JoshBruce\Site\PageComponents\DateBlock; @@ -24,9 +25,9 @@ class Markdown private string $body = ''; - public static function for(File $file): Markdown + public static function for(File $file, FileSystemInterface $in): Markdown { - return new Markdown($file); + return new Markdown($file, $in); } public static function markdownConverter(): MarkdownConverter @@ -49,8 +50,10 @@ public static function markdownConverter(): MarkdownConverter ); } - private function __construct(private File $file) - { + private function __construct( + private File $file, + private FileSystemInterface $fileSystem + ) { } public function html(): string @@ -77,10 +80,13 @@ public function html(): string $b = ''; $template = $templateMap[$templateKey]; if ($templateKey === 'loglist') { - $b = $template::create($this->file); + $b = $template::create($this->file(), $this->fileSystem()); } else { - $b = $template::create($this->frontMatter()); + $b = $template::create( + $this->frontMatter(), + $this->fileSystem() + ); } @@ -118,11 +124,11 @@ public function pageTitle(): string $titles = []; $titles[] = $this->frontMatter()->title(); - $file = clone $this->file; + $file = clone $this->file(); while ($file->canGoUp()) { $file = $file->up(); - $m = Markdown::for($file); + $m = Markdown::for($file, $this->fileSystem()); $titles[] = $m->frontMatter()->title(); } @@ -133,14 +139,24 @@ public function pageTitle(): string public function canonicalURl(): string { - return $this->file->canonicalUrl(); + return $this->file()->canonicalUrl(); } private function fileContent(): string { - if (strlen($this->fileContent) === 0 and $this->file->found()) { - $this->fileContent = $this->file->contents(); + if (strlen($this->fileContent) === 0 and $this->file()->found()) { + $this->fileContent = $this->file()->contents(); } return $this->fileContent; } + + private function file(): File + { + return $this->file; + } + + private function fileSystem(): FileSystemInterface + { + return $this->fileSystem; + } } diff --git a/src/Documents/Sitemap.php b/src/Documents/Sitemap.php index 2cc565da..2c72919e 100644 --- a/src/Documents/Sitemap.php +++ b/src/Documents/Sitemap.php @@ -7,23 +7,24 @@ use Eightfold\XMLBuilder\Document; use Eightfold\XMLBuilder\Element; -use JoshBruce\Site\FileSystem; +use JoshBruce\Site\FileSystemInterface; use JoshBruce\Site\File; use JoshBruce\Site\Content\Markdown; class Sitemap { - public static function create(): string + public static function create(FileSystemInterface $fileSystem): string { - $finder = FileSystem::finder()->name('content.md')->sortByName() + $finder = $fileSystem->publishedContentFinder()->sortByName() ->notContains('redirect:') ->notContains('noindex:'); $markdown = []; foreach ($finder as $file) { $markdown[] = Markdown::for( - File::at($file->getPathname()) + File::at($file->getPathname(), $fileSystem), + $fileSystem ); } diff --git a/src/File.php b/src/File.php index 6c88eb21..a5656318 100644 --- a/src/File.php +++ b/src/File.php @@ -6,19 +6,21 @@ use DirectoryIterator; -use JoshBruce\Site\FileSystem; +use JoshBruce\Site\FileSystemInterface; class File { private string $contentFileName = '/content.md'; - public static function at(string $localPath): File + public static function at(string $localPath, FileSystemInterface $in): File { - return new File($localPath); + return new File($localPath, $in); } - private function __construct(private string $localPath) - { + private function __construct( + private string $localPath, + private FileSystemInterface $fileSystem + ) { } public function isNotMarkdown(): bool @@ -60,7 +62,7 @@ public function path(bool $full = true): string } // TODO: test and verify used - returning empty string not an option. return str_replace( - $this->contentRoot(), + $this->fileSystem()->publicRoot(), '', $this->localPath ); @@ -76,7 +78,10 @@ public function up(): File $parts = explode('/', $this->localPath); $parts = array_slice($parts, 0, -2); // remove file name and one folder. $localPath = implode('/', $parts); - return File::at($localPath . $this->contentFileName); + return File::at( + $localPath . $this->contentFileName, + $this->fileSystem() + ); } public function contents(): string @@ -138,14 +143,15 @@ public function children(string $filesNamed): array $folderName = array_pop($parts); $files[$folderName] = File::at( - $fullPathToFolder . '/' . $filesNamed + $fullPathToFolder . '/' . $filesNamed, + $this->fileSystem() ); } return $files; } - private function contentRoot(): string + private function fileSystem(): FileSystemInterface { - return FileSystem::publicRoot(); + return $this->fileSystem; } } diff --git a/src/FileSystem.php b/src/FileSystem.php index 7855d15a..3d098e02 100644 --- a/src/FileSystem.php +++ b/src/FileSystem.php @@ -8,7 +8,9 @@ use Symfony\Component\Finder\Finder; -class FileSystem +use JoshBruce\Site\FileSystemInterface; + +class FileSystem implements FileSystemInterface { public static function init(): static { @@ -74,21 +76,6 @@ private function isDraft(SplFileInfo $finderFile): bool return str_contains($relativePath, '_'); } - private function projectRootFinder(): Finder - { - return $this->finder()->in(static::projectRoot()); - } - - private function contentRootFinder(): Finder - { - return $this->finder()->in($this->contentRoot()); - } - - private function publicRootFinder(): Finder - { - return $this->finder()->in($this->publicRoot()); - } - private function finder(): Finder { $finder = new Finder(); diff --git a/src/FileSystemInterface.php b/src/FileSystemInterface.php new file mode 100644 index 00000000..5a638bcd --- /dev/null +++ b/src/FileSystemInterface.php @@ -0,0 +1,22 @@ +serverGlobals()->appEnv() !== 'production') { // use Whoops! for error display @@ -84,7 +84,7 @@ public function isNotFound(): bool public function localFile(): File { - return File::at(localPath: $this->localPath()); + return File::at(localPath: $this->localPath(), in: $this->fileSystem()); } public function isFile(): bool @@ -102,6 +102,11 @@ public function isNotSitemap(): bool return ! $this->isSitemap(); } + public function fileSystem(): FileSystemInterface + { + return $this->fileSystem; + } + private function localPath(): string { if (empty($this->localPath)) { @@ -115,9 +120,9 @@ private function localPath(): string $relativePath = "/{$relativePath}"; } - $root = FileSystem::contentRoot(); + $root = $this->fileSystem()->publicRoot(); - $this->localPath = "{$root}/public{$relativePath}"; + $this->localPath = "{$root}{$relativePath}"; } return $this->localPath; } diff --git a/src/HttpResponse.php b/src/HttpResponse.php index f5d3ac76..f40e74dd 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -14,7 +14,6 @@ use Eightfold\HTMLBuilder\Document; use Eightfold\HTMLBuilder\Element; -use JoshBruce\Site\FileSystem; use JoshBruce\Site\File; use JoshBruce\Site\Content\Markdown; @@ -78,12 +77,14 @@ public function body(): string return $localFile->path(); } elseif ($this->statusCode() === 404) { - $localPath = FileSystem::contentRoot() . '/public/error-404.md'; - $localFile = File::at($localPath); + $localPath = $this->request()->fileSystem()->publicRoot() . + '/error-404.md'; + $localFile = File::at($localPath, $this->request()->fileSystem()); } elseif ($this->statusCode() === 405) { - $localPath = FileSystem::contentRoot() . '/public/error-405.md'; - $localFile = File::at($localPath); + $localPath = $this->request()->fileSystem()->publicRoot() . + '/public/error-405.md'; + $localFile = File::at($localPath, $this->request()->fileSystem()); } @@ -91,14 +92,17 @@ public function body(): string $pageTitle = ''; $html = ''; if ($localFile->isMarkdown()) { - $markdown = Markdown::for(file: $localFile); + $markdown = Markdown::for( + file: $localFile, + in: $this->request()->fileSystem() + ); $template = $markdown->frontMatter()->template(); $pageTitle = $markdown->pageTitle(); $html = $markdown->html(); } if ($this->request()->isSitemap()) { - return Sitemap::create(); + return Sitemap::create($this->request()->fileSystem()); } return Document::create( @@ -135,7 +139,7 @@ public function body(): string $html )->props('typeof BlogPosting', 'vocab https://schema.org/'), Element::a('top')->props('href #content-top', 'id go-to-top'), - Navigation::create('main.md'), + Navigation::create('main.md', $this->request()->fileSystem()), Element::footer( Element::p( 'Copyright © 2004–' . date('Y') . ' Joshua C. Bruce. ' . diff --git a/src/PageComponents/LogList.php b/src/PageComponents/LogList.php index 687571c5..75ca92ca 100644 --- a/src/PageComponents/LogList.php +++ b/src/PageComponents/LogList.php @@ -4,6 +4,7 @@ namespace JoshBruce\Site\PageComponents; +use JoshBruce\Site\FileSystemInterface; use JoshBruce\Site\File; use Eightfold\HTMLBuilder\Element; @@ -12,8 +13,10 @@ class LogList { - public static function create(File $file): string - { + public static function create( + File $file, + FileSystemInterface $fileSystem + ): string { $fileSubfolders = $file->children(filesNamed: 'content.md'); if (count($fileSubfolders) === 0) { return ''; @@ -23,7 +26,7 @@ public static function create(File $file): string $logLinks = []; foreach ($fileSubfolders as $key => $file) { if (! str_starts_with(strval($key), '_') and $file->found()) { - $markdown = Markdown::for($file); + $markdown = Markdown::for($file, $fileSystem); $linkPath = str_replace( '/content.md', diff --git a/src/PageComponents/Navigation.php b/src/PageComponents/Navigation.php index 93925813..325d5909 100644 --- a/src/PageComponents/Navigation.php +++ b/src/PageComponents/Navigation.php @@ -6,24 +6,26 @@ use Eightfold\HTMLBuilder\Element; -use JoshBruce\Site\FileSystem; +use JoshBruce\Site\FileSystemInterface; use JoshBruce\Site\File; use JoshBruce\Site\Content\Markdown; class Navigation { - public static function create(string $fileName): string - { - $contentRoot = FileSystem::contentRoot(); + public static function create( + string $fileName, + FileSystemInterface $fileSystem + ): string { + $contentRoot = $fileSystem->contentRoot(); $navigationPath = $contentRoot . '/navigation'; $filePath = $navigationPath . '/' . $fileName; - $file = File::at($filePath); + $file = File::at($filePath, $fileSystem); if ($file->isNotFound()) { return ''; } - $html = Markdown::for($file)->html(); + $html = Markdown::for($file, $fileSystem)->html(); return Element::nav($html)->props('id main-nav')->build(); } diff --git a/src/PageComponents/OriginalContentNotice.php b/src/PageComponents/OriginalContentNotice.php index 067671a2..bcd612c9 100644 --- a/src/PageComponents/OriginalContentNotice.php +++ b/src/PageComponents/OriginalContentNotice.php @@ -7,19 +7,22 @@ use Eightfold\HTMLBuilder\Element; use JoshBruce\Site\File; -use JoshBruce\Site\FileSystem; +use JoshBruce\Site\FileSystemInterface; use JoshBruce\Site\Content\Markdown; use JoshBruce\Site\Content\FrontMatter; class OriginalContentNotice { - public static function create(FrontMatter $frontMatter): string - { - $contentRoot = FileSystem::contentRoot(); + public static function create( + FrontMatter $frontMatter, + FileSystemInterface $fileSystem + ): string { + // $contentRoot = FileSystem::contentRoot(); + $contentRoot = $fileSystem->contentRoot(); $noticesRoot = $contentRoot . '/notices'; - $file = File::at($noticesRoot . '/original.md'); + $file = File::at($noticesRoot . '/original.md', $fileSystem); if ($file->isNotFound()) { return ''; } @@ -27,7 +30,7 @@ public static function create(FrontMatter $frontMatter): string $original = $frontMatter->original(); list($href, $platform) = explode(' ', $original, 2); - $body = Markdown::for($file)->body(); + $body = Markdown::for($file, $fileSystem)->body(); $matches = []; $search = '/{!!platformlink!!}/'; diff --git a/src/ServerGlobals.php b/src/ServerGlobals.php index ec1e1600..aa2102c3 100644 --- a/src/ServerGlobals.php +++ b/src/ServerGlobals.php @@ -21,11 +21,11 @@ private function __construct() $this->globals = $_SERVER; } - public function withRequestUri(string $method): ServerGlobals + public function withRequestUri(string $uri): ServerGlobals { $this->globals = []; - $_SERVER['REQUEST_URI'] = $method; + $_SERVER['REQUEST_URI'] = $uri; return $this; } diff --git a/src/SiteStatic/Generator.php b/src/SiteStatic/Generator.php index 7bf21e87..87e9bf1c 100644 --- a/src/SiteStatic/Generator.php +++ b/src/SiteStatic/Generator.php @@ -40,13 +40,14 @@ private function __construct( private OutputInterface $output, private string $destination = '' ) { - $projectRoot = FileSystem::projectRoot(); + $projectRoot = FileSystem::init()->projectRoot(); + // $projectRoot = FileSystem::projectRoot(); Dotenv::createImmutable($projectRoot)->load(); - $this->isNotTesting = ServerGlobals::init()->appEnv() !== 'test'; + // $this->isNotTesting = ServerGlobals::init()->appEnv() !== 'test'; - $this->contentRoot = FileSystem::contentRoot() . '/public'; + $this->contentRoot = FileSystem::init()->publicRoot(); if (strlen($destination) === 0) { $this->destination = $projectRoot . '/site-static-html/public'; @@ -82,21 +83,24 @@ private function compileContentFileFor(string $contentPath): void $parts = array_slice($parts, 0, -1); $requestUri = implode('/', $parts); - $request = HttpRequest::fromGlobals()->withUri($requestUri); + $globals = ServerGlobals::init()->withRequestUri($requestUri) + ->withRequestMethod('GET'); if (str_contains($destinationPath, '/error-404.html')) { - $request = HttpRequest::fromGlobals()->withUri( - '/low/probability/of/ex/is/ting' - ); + $globals = $globals->withRequestUri('/low/prob/a/bil/it/ee'); } elseif (str_contains($destinationPath, '/error-405.html')) { - $request = HttpRequest::fromGlobals()->withMethod('DELETE'); + $globals = $globals->withRequestMethod('DELETE'); } elseif (strlen($requestUri) === 0) { - $request = HttpRequest::fromGlobals()->withUri('/'); + $globals = $globals->withRequestUri('/'); } - $html = HttpResponse::from(request: $request)->body(); + $fileSystem = FileSystem::init(); + + $html = HttpResponse::from( + request: HttpRequest::with(serverGlobals: $globals, in: $fileSystem) + )->body(); $this->leagueFileSystem()->write($destinationPath, $html); diff --git a/tests/FileTest.php b/tests/FileTest.php index 956a10c5..1311aafb 100644 --- a/tests/FileTest.php +++ b/tests/FileTest.php @@ -4,31 +4,21 @@ use JoshBruce\Site\File; -use JoshBruce\Site\FileSystem; +use JoshBruce\Site\Tests\TestFileSystem; test('can generate canonical URL', function() { - expect( - File::at(FileSystem::publicRoot() . '/content.md')->canonicalUrl() - )->toBe( - 'https://joshbruce.com' - ); + $fileSystem = TestFileSystem::init(); + $publicRoot = $fileSystem->publicRoot(); expect( - File::at(FileSystem::publicRoot())->canonicalUrl() - )->toBe( + File::at($publicRoot . '/content.md', $fileSystem)->canonicalUrl() + )->toBe( 'https://joshbruce.com' - ); + ); expect( - File::at(FileSystem::publicRoot() . '/web-development')->canonicalUrl() - )->toBe( - 'https://joshbruce.com/web-development' - ); - - expect( - File::at(FileSystem::publicRoot() . '/web-development/content.md') - ->canonicalUrl() + File::at($publicRoot, $fileSystem)->canonicalUrl() )->toBe( - 'https://joshbruce.com/web-development' + 'https://joshbruce.com' ); })->group('file'); diff --git a/tests/HttpTest.php b/tests/HttpTest.php index 338a01e2..4e93da46 100644 --- a/tests/HttpTest.php +++ b/tests/HttpTest.php @@ -32,7 +32,7 @@ )->toBe( ['Content-Type' => 'text/css'] ); -})->group('response', 'request', 'server-globals'); +})->group('response', 'request'); test('expected titles', function() { $body = HttpResponse::from( @@ -43,12 +43,12 @@ )->body(); expect( - str_contains($body, "Josh Bruce's personal site") + str_contains($body, "Test content root") )->toBeTrue(); $body = HttpResponse::from( request: HttpRequest::with( - ServerGlobals::init()->withRequestUri('/finances'), + ServerGlobals::init()->withRequestUri('/published-sub'), TestFileSystem::init() ) )->body(); @@ -56,7 +56,7 @@ expect( str_contains( $body, - "Finances | Josh Bruce's personal site" + "Sub-folder content title | Test content root" ) )->toBeTrue(); @@ -73,7 +73,7 @@ "Page not found" ) )->toBeTrue(); -})->group('response', 'request', 'server-globals'); +})->group('response', 'request'); test('expected status codes', function() { expect( @@ -123,4 +123,4 @@ ); $_SERVER['APP_URL'] = 'http://jb-site.test'; -})->group('response', 'request', 'server-globals'); +})->group('response', 'request'); diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php index 752cfc9c..49375aee 100644 --- a/tests/SitemapTest.php +++ b/tests/SitemapTest.php @@ -7,11 +7,21 @@ use JoshBruce\Site\HttpRequest; use JoshBruce\Site\HttpResponse; +use JoshBruce\Site\ServerGlobals; + +use JoshBruce\Site\Tests\TestFileSystem; + + it('can respond to sitemap request', function() { - serverGlobals('sitemap.xml'); + // serverGlobals('sitemap.xml'); $xml = HttpResponse::from( - request: HttpRequest::fromGlobals() + request: HttpRequest::with( + ServerGlobals::init() + ->withRequestUri('/sitemap.xml') + ->withRequestMethod('GET'), // TODO: indicates fragile tests + TestFileSystem::init() + ) ); expect( @@ -34,7 +44,7 @@ $xml->body() )->toBe(<< - https://joshbruce.comhttps://joshbruce.com/design-your-lifehttps://joshbruce.com/design-your-life/motivatorshttps://joshbruce.com/finances/budgetinghttps://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210301https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210315https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210401https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210415https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210501https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210515https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210601https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210615https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210701https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210715https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210801https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210815https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210901https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20210915https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211001https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211015https://joshbruce.com/finances/building-wealth-paycheck-to-paycheck/20211101https://joshbruce.com/finances/building-wealth-paycheck-to-paycheckhttps://joshbruce.com/financeshttps://joshbruce.com/finances/investment-policyhttps://joshbruce.com/health-and-wellnesshttps://joshbruce.com/legalhttps://joshbruce.com/software-developmenthttps://joshbruce.com/software-development/why-dont-you-usehttps://joshbruce.com/web-development/2021-site-in-depthhttps://joshbruce.com/web-developmenthttps://joshbruce.com/web-development/modern-web-developmenthttps://joshbruce.com/web-development/my-history-on-the-webhttps://joshbruce.com/web-development/on-constraintshttps://joshbruce.com/web-development/on-constraints/internet-bandwidthhttps://joshbruce.com/web-development/refactoring-re-engineering-and-rebuildinghttps://joshbruce.com/web-development/site-statshttps://joshbruce.com/web-development/static-dynamic-and-interactive + https://joshbruce.comhttps://joshbruce.com/published-sub xml ); -})->group('sitemap'); +})->group('request', 'response', 'sitemap'); diff --git a/tests/test-content/content/public/assets/css/main.css b/tests/test-content/content/public/assets/css/main.css new file mode 100644 index 00000000..cc3bd7f9 --- /dev/null +++ b/tests/test-content/content/public/assets/css/main.css @@ -0,0 +1,372 @@ +@charset "UTF-8"; +:root{ + --header-font:"Gill Sans", "Gill Sans MT", Calibri, sans-serif; + --body-font:"Open Sans", "Helvetica Neue", Verdana, sans-serif; + --code-font:Consolas, monaco, monospace; + --x-bold-font:600; + --bold-font:500; + --medium-font:300; + --light-font:250; + --2xl-font:clamp(30pt, 9.375vw, 35pt); + --xl-font:clamp(25pt, 7.8125vw, 30pt); + --l-font:clamp(20pt, 6.25vw, 25pt); + --m-font:clamp(16pt, 5vw, 18pt); + --s-font:clamp(13pt, 4.0625vw, 15pt); + --xs-font:clamp(10pt, 3.125vw, 14pt); + --l-line-height:calc(var(--2xl-font) * 1); + --m-line-height:calc(var(--s-font) * 1.62); + --s-line-height:calc(var(--s-font) * 1.25); + --line-length:70ch; + --key:#0A6276; + --key-light:#5FCAF2; + --gray-darkest:#030303; + --gray-darker:#0F2124; + --gray-lightest:#FCFDFD; + --vw-0:0vw; + --0-spacer:0; + --xl-spacer:4rem; + --l-spacer:2rem; + --m-spacer:1rem; + --s-spacer:0.75rem; + --xs-spacer:0.5rem; + --2xs-spacer:0.25rem; + --3xs-spacer:0.1rem; + --1-px:1px; + --2-px:2px; + --3-px:3px; + --400-px:400px; + --icon-new-window:"⧉"; + --icon-increase:"◭"; + --icon-decrease:"⧩"; + --icon-hold:"≅"; + --icon-lift-spacer:-0.5rem; + --margin-centered:var(--0-spacer) var(--auto); + --smooth:smooth; + --auto:auto; + --pointer:pointer; + --none:none; + --transition-color:color 0.25s; + --center:center; + --left:left; + --inline-block:inline-block; + --solid:solid; + --dashed:dashed; + --transparent:transparent; + --relative:relative; + --absolute:absolute; + --italic:italic; + --block:block; + --both:both; + --hidden:hidden; +} + +body{ + min-height:var(--vw-0); + max-width:var(--line-length); + margin:var(--margin-centered); + background:var(--gray-lightest); + color:var(--gray-darkest); + font-family:var(--body-font); + font-size:var(--m-font); + line-height:var(--m-line-height); + font-weight:var(--light-font); +} +body > a{ + position:var(--relative); + text-align:var(--center); + font-size:var(--s-font); + display:var(--block); + margin:var(--l-spacer); + font-weight:var(--light-font); + padding:var(--m-spacer); +} +body > a[id=content-top]:before{ + display:var(--inline-block); + content:var(--icon-decrease); + position:var(--absolute); + left:var(--0-spacer); +} +body > a[id=content-top]:after{ + display:var(--inline-block); + content:var(--icon-decrease); + position:var(--absolute); + right:var(--0-spacer); +} +body > a[id=go-to-top]:before{ + display:var(--inline-block); + content:var(--icon-increase); + position:var(--absolute); + left:var(--0-spacer); +} +body > a[id=go-to-top]:after{ + display:var(--inline-block); + content:var(--icon-increase); + position:var(--absolute); + right:var(--0-spacer); +} + +a:hover{ + cursor:var(--pointer); +} + +a{ + font-weight:var(--bold-font); + -webkit-text-decoration:var(--none); + text-decoration:var(--none); + color:var(--key); + transition:var(--transition-color); +} +a:hover{ + color:var(--gray-darker); +} + +[is=dateblock]{ + margin-bottom:var(--l-spacer); +} +[is=dateblock] p{ + margin:var(--0-spacer); + font-size:var(--xs-font); +} +[is=dateblock] p > time{ + font-weight:var(--bold-font); +} + +h1, +h2, +h3, +h4, +h5, +h6{ + margin-top:var(--l-spacer); + margin-bottom:var(--xs-spacer); + font-family:var(--head-font); + font-size:clamp(30pt, 9.375vw, 35pt); + line-height:var(--l-line-height); +} +h1 + ol, h1 + ul, h1 + p, +h2 + ol, +h2 + ul, +h2 + p, +h3 + ol, +h3 + ul, +h3 + p, +h4 + ol, +h4 + ul, +h4 + p, +h5 + ol, +h5 + ul, +h5 + p, +h6 + ol, +h6 + ul, +h6 + p{ + margin-top:var(--0-spacer); + padding-top:var(--0-spacer); +} + +h1{ + margin-top:var(--m-spacer); +} + +h2{ + font-size:var(--xl-font); +} + +h3, +h4, +h5, +h6{ + font-size:var(--l-font); + text-align:var(--center); +} + +hr{ + width:33%; + border:var(--2-px) var(--solid) var(--gray-darkest); + border-radius:var(--1-px); +} + +code{ + font-family:var(--code-font); + font-size:var(--s-font); +} + +.toc{ + padding:var(--none); + list-style:var(--none); +} +.toc li{ + border-bottom:var(--1-px) var(--solid) var(--gray-darker); +} +.toc li:last-child{ + border-bottom:var(--none); +} +.toc h3{ + margin:var(--xs-spacer); + text-align:var(--left); + font-size:var(--l-font); +} +.toc h3 a{ + display:var(--block); + padding:var(--m-spacer); +} +.toc h3 small{ + font-size:var(--s-font); + display:var(--block); + color:var(--gray-darker); + font-weight:var(--light-font); +} + +main, article{ + padding:var(--0-spacer) var(--m-spacer); + padding-top:var(--m-spacer); +} +main img, +main iframe[src*="https://jsfiddle.net"], article img, +article iframe[src*="https://jsfiddle.net"]{ + display:var(--block); + margin:var(--margin-centered); + width:80%; + border:var(--3-px) var(--solid) var(--gray-darker); +} +main iframe[src*="https://jsfiddle.net"], article iframe[src*="https://jsfiddle.net"]{ + min-height:300px; +} +main abbr, article abbr{ + -webkit-text-decoration:var(--none); + text-decoration:var(--none); + border-bottom:var(--1-px) var(--dashed) var(--gray-darker); +} +main dl > dt, article dl > dt{ + margin-top:var(--m-spacer); +} +main dl > dd, article dl > dd{ + margin-bottom:var(--s-spacer); +} +main details, article details{ + border:var(--1-px) var(--solid) var(--key); + border-radius:var(--3-px); + margin-top:var(--m-spacer); + padding:0.5em 0.5em 0; + font-weight:var(--bold-font); +} +main details summary, article details summary{ + color:var(--key); + cursor:var(--pointer); + margin:-0.5em -0.5em 0; + padding:0.5em; +} + +nav > ul{ + margin:var(--0-spacer); + padding:var(--0-spacer); + list-style:var(--none); +} +nav > ul > li:first-of-type{ + margin-bottom:var(--m-spacer); +} +nav > ul ul{ + list-style:var(--none); + margin:var(--0-spacer) var(--0-spacer); + padding:var(--0-spacer) var(--0-spacer); +} +nav > ul ul a{ + font-size:var(--s-font); +} +nav > ul ul li:last-of-type a{ + padding-bottom:var(--s-spacer); +} +nav > ul a{ + display:var(--block); + overflow:var(--hidden); + text-align:var(--center); + position:var(--relative); +} +nav > ul abbr{ + border-color:var(--gray-lightest); +} + +@media (prefers-reduced-motion: no-preference){ + html{ + scroll-behavior:var(--smooth); + } + + nav > ul a:after{ + content:""; + position:var(--absolute); + width:25%; + border-top:var(--1-px) var(--solid) var(--gray-darker); + opacity:0.25; + top:60%; + left:-100%; + transition-delay:all 0.75s; + transition:all 0.75s ease-out; + } + nav > ul a:hover:after{ + left:100%; + } +} +@media (prefers-color-scheme: dark){ + body{ + background:var(--gray-darkest); + color:var(--gray-lightest); + font-weight:var(--medium-font); + } + + hr{ + border:1px solid var(--gray-lightest); + } + + a{ + color:var(--key-light); + font-weight:var(--x-bold-font); + } + a:hover{ + color:var(--gray-lightest); + } + + [is=dateblock] p > time, b, strong{ + font-weight:var(--x-bold-font); + } + + nav > ul a:after{ + border-top:var(--1-px) var(--solid) var(--gray-lightest); + } + + main img, +main iframe[src*="https://jsfiddle.net"], article img, +article iframe[src*="https://jsfiddle.net"]{ + border:3px solid var(--gray-lightest); + } + main details, article details{ + font-weight:var(--bold-font); + } + main details summary, article details summary{ + color:var(--key-light); + } +} +blockquote{ + margin:var(--m-spacer); + font-style:var(--italic); +} + +.heading-permalink{ + display:var(--inline-block); + margin-right:var(--2xs-spacer); +} + +a[rel~=noreferrer]:after{ + content:var(--icon-new-window); + display:var(--inline-block); + margin-left:var(--2xs-spacer); + position:var(--relative); + top:var(--icon-lift-spacer); + font-size:var(--xs-font); +} + +footer{ + margin-top:var(--l-spacer); +} +footer > p{ + text-align:var(--center); + font-size:var(--xs-font); +} \ No newline at end of file diff --git a/tests/test-content/content/public/assets/css/main.min.css b/tests/test-content/content/public/assets/css/main.min.css new file mode 100644 index 00000000..be9e0d49 --- /dev/null +++ b/tests/test-content/content/public/assets/css/main.min.css @@ -0,0 +1,2 @@ +@charset "UTF-8";:root{--header-font:"Gill Sans", "Gill Sans MT", Calibri, sans-serif;--body-font:"Open Sans", "Helvetica Neue", Verdana, sans-serif;--code-font:Consolas, monaco, monospace;--x-bold-font:600;--bold-font:500;--medium-font:300;--light-font:250;--2xl-font:clamp(30pt, 9.375vw, 35pt);--xl-font:clamp(25pt, 7.8125vw, 30pt);--l-font:clamp(20pt, 6.25vw, 25pt);--m-font:clamp(16pt, 5vw, 18pt);--s-font:clamp(13pt, 4.0625vw, 15pt);--xs-font:clamp(10pt, 3.125vw, 14pt);--l-line-height:calc(var(--2xl-font) * 1);--m-line-height:calc(var(--s-font) * 1.62);--s-line-height:calc(var(--s-font) * 1.25);--line-length:70ch;--key:#0A6276;--key-light:#5FCAF2;--gray-darkest:#030303;--gray-darker:#0F2124;--gray-lightest:#FCFDFD;--vw-0:0vw;--0-spacer:0;--xl-spacer:4rem;--l-spacer:2rem;--m-spacer:1rem;--s-spacer:0.75rem;--xs-spacer:0.5rem;--2xs-spacer:0.25rem;--3xs-spacer:0.1rem;--1-px:1px;--2-px:2px;--3-px:3px;--400-px:400px;--icon-new-window:"⧉";--icon-increase:"◭";--icon-decrease:"⧩";--icon-hold:"≅";--icon-lift-spacer:-0.5rem;--margin-centered:var(--0-spacer) var(--auto);--smooth:smooth;--auto:auto;--pointer:pointer;--none:none;--transition-color:color 0.25s;--center:center;--left:left;--inline-block:inline-block;--solid:solid;--dashed:dashed;--transparent:transparent;--relative:relative;--absolute:absolute;--italic:italic;--block:block;--both:both;--hidden:hidden}body{min-height:var(--vw-0);max-width:var(--line-length);margin:var(--margin-centered);background:var(--gray-lightest);color:var(--gray-darkest);font-family:var(--body-font);font-size:var(--m-font);line-height:var(--m-line-height)}body>a,code{font-size:var(--s-font)}body,body>a{font-weight:var(--light-font)}body>a{position:var(--relative);text-align:var(--center);display:var(--block);margin:var(--l-spacer);padding:var(--m-spacer)}body>a[id=content-top]:after,body>a[id=content-top]:before{display:var(--inline-block);content:var(--icon-decrease);position:var(--absolute)}body>a[id=content-top]:before{left:var(--0-spacer)}body>a[id=content-top]:after{right:var(--0-spacer)}body>a[id=go-to-top]:after,body>a[id=go-to-top]:before{display:var(--inline-block);content:var(--icon-increase);position:var(--absolute)}body>a[id=go-to-top]:before{left:var(--0-spacer)}body>a[id=go-to-top]:after{right:var(--0-spacer)}a:hover{cursor:var(--pointer);color:var(--gray-darker)}a{-webkit-text-decoration:var(--none);text-decoration:var(--none);color:var(--key);transition:var(--transition-color)}[is=dateblock]{margin-bottom:var(--l-spacer)}[is=dateblock] p{margin:var(--0-spacer);font-size:var(--xs-font)}[is=dateblock] p>time,a{font-weight:var(--bold-font)}h1,h2,h3,h4,h5,h6{margin-bottom:var(--xs-spacer);font-family:var(--head-font);font-size:clamp(30pt,9.375vw,35pt);line-height:var(--l-line-height)}h2,h3,h4,h5,h6{margin-top:var(--l-spacer)}h1+ol,h1+p,h1+ul,h2+ol,h2+p,h2+ul,h3+ol,h3+p,h3+ul,h4+ol,h4+p,h4+ul,h5+ol,h5+p,h5+ul,h6+ol,h6+p,h6+ul{margin-top:var(--0-spacer);padding-top:var(--0-spacer)}article dl>dt,h1,main dl>dt{margin-top:var(--m-spacer)}h2{font-size:var(--xl-font)}h3,h4,h5,h6{font-size:var(--l-font);text-align:var(--center)}hr{width:33%;border:var(--2-px) var(--solid) var(--gray-darkest);border-radius:var(--1-px)}code{font-family:var(--code-font)}.toc{padding:var(--none);list-style:var(--none)}.toc li{border-bottom:var(--1-px) var(--solid) var(--gray-darker)}.toc li:last-child{border-bottom:var(--none)}.toc h3{margin:var(--xs-spacer);text-align:var(--left);font-size:var(--l-font)}.toc h3 a{display:var(--block);padding:var(--m-spacer)}.toc h3 small{font-size:var(--s-font);display:var(--block);color:var(--gray-darker);font-weight:var(--light-font)}article,main{padding:var(--0-spacer) var(--m-spacer);padding-top:var(--m-spacer)}article iframe[src*="https://jsfiddle.net"],article img,main iframe[src*="https://jsfiddle.net"],main img{display:var(--block);margin:var(--margin-centered);width:80%;border:var(--3-px) var(--solid) var(--gray-darker)}article iframe[src*="https://jsfiddle.net"],main iframe[src*="https://jsfiddle.net"]{min-height:300px}article abbr,main abbr{-webkit-text-decoration:var(--none);text-decoration:var(--none);border-bottom:var(--1-px) var(--dashed) var(--gray-darker)}article dl>dd,main dl>dd{margin-bottom:var(--s-spacer)}article details,main details{border:var(--1-px) var(--solid) var(--key);border-radius:var(--3-px);margin-top:var(--m-spacer);padding:.5em .5em 0;font-weight:var(--bold-font)}article details summary,main details summary{color:var(--key);cursor:var(--pointer);margin:-.5em -.5em 0;padding:.5em}nav>ul{margin:var(--0-spacer);padding:var(--0-spacer);list-style:var(--none)}nav>ul>li:first-of-type{margin-bottom:var(--m-spacer)}nav>ul ul{list-style:var(--none);margin:var(--0-spacer) var(--0-spacer);padding:var(--0-spacer) var(--0-spacer)}nav>ul ul a{font-size:var(--s-font)}nav>ul ul li:last-of-type a{padding-bottom:var(--s-spacer)}footer>p,nav>ul a{text-align:var(--center)}nav>ul a{display:var(--block);overflow:var(--hidden);position:var(--relative)}nav>ul abbr{border-color:var(--gray-lightest)}@media (prefers-reduced-motion:no-preference){html{scroll-behavior:var(--smooth)}nav>ul a:after{content:"";position:var(--absolute);width:25%;border-top:var(--1-px) var(--solid) var(--gray-darker);opacity:.25;top:60%;left:-100%;transition-delay:all .75s;transition:all .75s ease-out}nav>ul a:hover:after{left:100%}}@media (prefers-color-scheme:dark){body{background:var(--gray-darkest);font-weight:var(--medium-font)}hr{border:1px solid var(--gray-lightest)}a:hover,body{color:var(--gray-lightest)}[is=dateblock] p>time,a,b,strong{font-weight:var(--x-bold-font)}nav>ul a:after{border-top:var(--1-px) var(--solid) var(--gray-lightest)}article iframe[src*="https://jsfiddle.net"],article img,main iframe[src*="https://jsfiddle.net"],main img{border:3px solid var(--gray-lightest)}article details,main details{font-weight:var(--bold-font)}a,article details summary,main details summary{color:var(--key-light)}}blockquote{margin:var(--m-spacer);font-style:var(--italic)}.heading-permalink{display:var(--inline-block);margin-right:var(--2xs-spacer)}a[rel~=noreferrer]:after{content:var(--icon-new-window);display:var(--inline-block);margin-left:var(--2xs-spacer);position:var(--relative);top:var(--icon-lift-spacer);font-size:var(--xs-font)}footer{margin-top:var(--l-spacer)}footer>p{font-size:var(--xs-font)} +/*# sourceMappingURL=main.min.css.map */ diff --git a/tests/test-content/content/public/assets/css/main.min.css.map b/tests/test-content/content/public/assets/css/main.min.css.map new file mode 100644 index 00000000..6dbb27c9 --- /dev/null +++ b/tests/test-content/content/public/assets/css/main.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["main.css","../../../main.scss",""],"names":[],"mappings":"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","file":"main.min.css","sourcesContent":["@charset \"UTF-8\";\n:root {\n --header-font: \"Gill Sans\", \"Gill Sans MT\", Calibri, sans-serif;\n --body-font: \"Open Sans\", \"Helvetica Neue\", Verdana, sans-serif;\n --code-font: Consolas, monaco, monospace;\n --x-bold-font: 600;\n --bold-font: 500;\n --medium-font: 300;\n --light-font: 250;\n --2xl-font: clamp(30pt, 9.375vw, 35pt);\n --xl-font: clamp(25pt, 7.8125vw, 30pt);\n --l-font: clamp(20pt, 6.25vw, 25pt);\n --m-font: clamp(16pt, 5vw, 18pt);\n --s-font: clamp(13pt, 4.0625vw, 15pt);\n --xs-font: clamp(10pt, 3.125vw, 14pt);\n --l-line-height: calc(var(--2xl-font) * 1);\n --m-line-height: calc(var(--s-font) * 1.62);\n --s-line-height: calc(var(--s-font) * 1.25);\n --line-length: 70ch;\n --key: #0A6276;\n --key-light: #5FCAF2;\n --gray-darkest: #030303;\n --gray-darker: #0F2124;\n --gray-lightest: #FCFDFD;\n --vw-0: 0vw;\n --0-spacer: 0;\n --xl-spacer: 4rem;\n --l-spacer: 2rem;\n --m-spacer: 1rem;\n --s-spacer: 0.75rem;\n --xs-spacer: 0.5rem;\n --2xs-spacer: 0.25rem;\n --3xs-spacer: 0.1rem;\n --1-px: 1px;\n --2-px: 2px;\n --3-px: 3px;\n --400-px: 400px;\n --icon-new-window: \"⧉\";\n --icon-increase: \"◭\";\n --icon-decrease: \"⧩\";\n --icon-hold: \"≅\";\n --icon-lift-spacer: -0.5rem;\n --margin-centered: var(--0-spacer) var(--auto);\n --smooth: smooth;\n --auto: auto;\n --pointer: pointer;\n --none: none;\n --transition-color: color 0.25s;\n --center: center;\n --left: left;\n --inline-block: inline-block;\n --solid: solid;\n --dashed: dashed;\n --transparent: transparent;\n --relative: relative;\n --absolute: absolute;\n --italic: italic;\n --block: block;\n --both: both;\n --hidden: hidden;\n}\n\nbody {\n min-height: var(--vw-0);\n max-width: var(--line-length);\n margin: var(--margin-centered);\n background: var(--gray-lightest);\n color: var(--gray-darkest);\n font-family: var(--body-font);\n font-size: var(--m-font);\n line-height: var(--m-line-height);\n font-weight: var(--light-font);\n}\nbody > a {\n position: var(--relative);\n text-align: var(--center);\n font-size: var(--s-font);\n display: var(--block);\n margin: var(--l-spacer);\n font-weight: var(--light-font);\n padding: var(--m-spacer);\n}\nbody > a[id=content-top]:before {\n display: var(--inline-block);\n content: var(--icon-decrease);\n position: var(--absolute);\n left: var(--0-spacer);\n}\nbody > a[id=content-top]:after {\n display: var(--inline-block);\n content: var(--icon-decrease);\n position: var(--absolute);\n right: var(--0-spacer);\n}\nbody > a[id=go-to-top]:before {\n display: var(--inline-block);\n content: var(--icon-increase);\n position: var(--absolute);\n left: var(--0-spacer);\n}\nbody > a[id=go-to-top]:after {\n display: var(--inline-block);\n content: var(--icon-increase);\n position: var(--absolute);\n right: var(--0-spacer);\n}\n\na:hover {\n cursor: var(--pointer);\n}\n\na {\n font-weight: var(--bold-font);\n text-decoration: var(--none);\n color: var(--key);\n transition: var(--transition-color);\n}\na:hover {\n color: var(--gray-darker);\n}\n\n[is=dateblock] {\n margin-bottom: var(--l-spacer);\n}\n[is=dateblock] p {\n margin: var(--0-spacer);\n font-size: var(--xs-font);\n}\n[is=dateblock] p > time {\n font-weight: var(--bold-font);\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n margin-top: var(--l-spacer);\n margin-bottom: var(--xs-spacer);\n font-family: var(--head-font);\n font-size: clamp(30pt, 9.375vw, 35pt);\n line-height: var(--l-line-height);\n}\nh1 + ol, h1 + ul, h1 + p,\nh2 + ol,\nh2 + ul,\nh2 + p,\nh3 + ol,\nh3 + ul,\nh3 + p,\nh4 + ol,\nh4 + ul,\nh4 + p,\nh5 + ol,\nh5 + ul,\nh5 + p,\nh6 + ol,\nh6 + ul,\nh6 + p {\n margin-top: var(--0-spacer);\n padding-top: var(--0-spacer);\n}\n\nh1 {\n margin-top: var(--m-spacer);\n}\n\nh2 {\n font-size: var(--xl-font);\n}\n\nh3,\nh4,\nh5,\nh6 {\n font-size: var(--l-font);\n text-align: var(--center);\n}\n\nhr {\n width: 33%;\n border: var(--2-px) var(--solid) var(--gray-darkest);\n border-radius: var(--1-px);\n}\n\ncode {\n font-family: var(--code-font);\n font-size: var(--s-font);\n}\n\n.toc {\n padding: var(--none);\n list-style: var(--none);\n}\n.toc li {\n border-bottom: var(--1-px) var(--solid) var(--gray-darker);\n}\n.toc li:last-child {\n border-bottom: var(--none);\n}\n.toc h3 {\n margin: var(--xs-spacer);\n text-align: var(--left);\n font-size: var(--l-font);\n}\n.toc h3 a {\n display: var(--block);\n padding: var(--m-spacer);\n}\n.toc h3 small {\n font-size: var(--s-font);\n display: var(--block);\n color: var(--gray-darker);\n font-weight: var(--light-font);\n}\n\nmain, article {\n padding: var(--0-spacer) var(--m-spacer);\n padding-top: var(--m-spacer);\n}\nmain img,\nmain iframe[src*=\"https://jsfiddle.net\"], article img,\narticle iframe[src*=\"https://jsfiddle.net\"] {\n display: var(--block);\n margin: var(--margin-centered);\n width: 80%;\n border: var(--3-px) var(--solid) var(--gray-darker);\n}\nmain iframe[src*=\"https://jsfiddle.net\"], article iframe[src*=\"https://jsfiddle.net\"] {\n min-height: 300px;\n}\nmain abbr, article abbr {\n text-decoration: var(--none);\n border-bottom: var(--1-px) var(--dashed) var(--gray-darker);\n}\nmain dl > dt, article dl > dt {\n margin-top: var(--m-spacer);\n}\nmain dl > dd, article dl > dd {\n margin-bottom: var(--s-spacer);\n}\nmain details, article details {\n border: var(--1-px) var(--solid) var(--key);\n border-radius: var(--3-px);\n margin-top: var(--m-spacer);\n padding: 0.5em 0.5em 0;\n font-weight: var(--bold-font);\n}\nmain details summary, article details summary {\n color: var(--key);\n cursor: var(--pointer);\n margin: -0.5em -0.5em 0;\n padding: 0.5em;\n}\n\nnav > ul {\n margin: var(--0-spacer);\n padding: var(--0-spacer);\n list-style: var(--none);\n}\nnav > ul > li:first-of-type {\n margin-bottom: var(--m-spacer);\n}\nnav > ul ul {\n list-style: var(--none);\n margin: var(--0-spacer) var(--0-spacer);\n padding: var(--0-spacer) var(--0-spacer);\n}\nnav > ul ul a {\n font-size: var(--s-font);\n}\nnav > ul ul li:last-of-type a {\n padding-bottom: var(--s-spacer);\n}\nnav > ul a {\n display: var(--block);\n overflow: var(--hidden);\n text-align: var(--center);\n position: var(--relative);\n}\nnav > ul abbr {\n border-color: var(--gray-lightest);\n}\n\n@media (prefers-reduced-motion: no-preference) {\n html {\n scroll-behavior: var(--smooth);\n }\n\n nav > ul a:after {\n content: \"\";\n position: var(--absolute);\n width: 25%;\n border-top: var(--1-px) var(--solid) var(--gray-darker);\n opacity: 0.25;\n top: 60%;\n left: -100%;\n transition-delay: all 0.75s;\n transition: all 0.75s ease-out;\n }\n nav > ul a:hover:after {\n left: 100%;\n }\n}\n@media (prefers-color-scheme: dark) {\n body {\n background: var(--gray-darkest);\n color: var(--gray-lightest);\n font-weight: var(--medium-font);\n }\n\n hr {\n border: 1px solid var(--gray-lightest);\n }\n\n a {\n color: var(--key-light);\n font-weight: var(--x-bold-font);\n }\n a:hover {\n color: var(--gray-lightest);\n }\n\n [is=dateblock] p > time, b, strong {\n font-weight: var(--x-bold-font);\n }\n\n nav > ul a:after {\n border-top: var(--1-px) var(--solid) var(--gray-lightest);\n }\n\n main img,\nmain iframe[src*=\"https://jsfiddle.net\"], article img,\narticle iframe[src*=\"https://jsfiddle.net\"] {\n border: 3px solid var(--gray-lightest);\n }\n main details, article details {\n font-weight: var(--bold-font);\n }\n main details summary, article details summary {\n color: var(--key-light);\n }\n}\nblockquote {\n margin: var(--m-spacer);\n font-style: var(--italic);\n}\n\n.heading-permalink {\n display: var(--inline-block);\n margin-right: var(--2xs-spacer);\n}\n\na[rel~=noreferrer]:after {\n content: var(--icon-new-window);\n display: var(--inline-block);\n margin-left: var(--2xs-spacer);\n position: var(--relative);\n top: var(--icon-lift-spacer);\n font-size: var(--xs-font);\n}\n\nfooter {\n margin-top: var(--l-spacer);\n}\nfooter > p {\n text-align: var(--center);\n font-size: var(--xs-font);\n}\n\n/*# sourceMappingURL=file:///Users/alex/Public/joshbruce-repos/site-joshbruce.com/content/assets/sass/main.scss */",null,null]} \ No newline at end of file diff --git a/tests/test-content/content/public/assets/favicons/android-chrome-192x192.png b/tests/test-content/content/public/assets/favicons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..b0cf626268182a1fb009914890674ef2bf75300b GIT binary patch literal 5553 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 OOSRSYZog5riTEG!&=zn2 literal 0 HcmV?d00001 diff --git a/tests/test-content/content/public/assets/favicons/android-chrome-512x512.png b/tests/test-content/content/public/assets/favicons/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..59fcfd54a4a0f26f8e7ed7820b4df32553fa7f5e GIT binary patch literal 15975 zcmeHug;!MV_wSj3L0S+b6;O~aB_&4$F=!NNPz0ormK+d81pz5VYLE~m1*Bsvy1N+# zBn6~v;67)3zwh^V?_Y4&x|g+B40FzTVm~`R`?L3XrlX}!L&-)7K@iQgtEzesgajXv z5IGt6v*FXX2mT=3_0(0Mf{s)3;2%|M!)rF0nvfv)Ob#K!>>yJ37Vygkej$hii-1VL zUj+Oe7Wtp6NC->v-_P)kR6Cy@Ly%(kHPuTueGtFLk0qb%uf_d2+WKPP#XN*cpzV1n z$bU8jMXF1$`k76tS)XCDcQ#y1Y$j1v-(R5Nrr))E!a&F?6yMlSihMo` zK7#)-+888;1c4x>LqiA>6hB0*A$NwGcnyufo&V>iU?d|%#+wpN4{jo*!$No#;s-Pi zji7``VzruxBa`buuGH`pW?`h7meN z^7mO5el)_;vWJ58?@}nC$iE**sTr}5xcGc%#NYL!pvHeTLPn_rvAp(BxdM-i2HUVA ze)xZF8&^ZQ1&*uNFSwEgvv27#J(cV*N^=5NU*&3t`7LqssT03~ux zAXvirOfXx;h5u-dYdxCq(W-cHm44DeEyFd2;ly-=^uxLkY}75rjgQZfw@yuGzmqnO zjpC)!fnFyE+*OAV+LSj3`CC7Op2inPsEx#H}E;>6TKYtcb-P}K56kJkWYD>pGXuaM? zs}ChFe1`9Zf_X_>@+F`J4UF&m;96&8m#1wkJ)<(wx(rF;CRT_*WS8tk4;o-QUAHV9 z=I>NC%5jV1gBz!RY$vR(V+x+xXIn5r?f4>c*u1b0gB7jSXVeSp#Rw}ws)152FHhy= zI0lj=*mI*1SWUq>a$wPTbHpZBZdxayOOTW!e*f?jdW%EA+eQ4z1`uPkDg!ULhmIzg z`&R#2wfCzX%HU&pA((e2S1v2o1n5AavkeABXV6X#w58?IfHv|gr+?2JU%K>V#&tDD z=)5-KH0(ucgs`Ga$%oUzlMb!HmnUfY9>3IuD#YzPVCUhQgiDr6$}2%=Hy^bh4Q=mO zm2Xal6SAPyy9UoF;O*+53BuSXtV;VwsWIg@>bi=;Y&@SKae4j>BJld$gWLCyx5TMi z#8g|nu}z`LdGRnv<2UeU?-Ti)fhf2dklMn==a>%nt+(n{4XJx{z$kjVa=` zKT|4fe%y@3eLae%IpCoaHd0;Gg;c*aHW06RQTluRXiTY4;`3>2r_EvPjHSC5KjcW3brIeqVjp$5 zIZRf$*3CSlqZUti!9H>pg-L(L=LnDKgyeKO)Z;yve>m)&Ip$07eG$95YRU^xpt#LO(Z{bx>GqbqurTlSS$VVtm^$~cE z<;<#9Gt=&}^6YiRZSp56E|i#}`5`)Z7hDNQF<}cK<%d&`481=0?`K1_BBFY4*)FJ?P8mKFk)oUxrHKITQ?UpI?6h}v|A z^j(2t{e1Bw@o+Fyq^YK**UgVS9#Q$S?ThL`-;|DR5F-`tO7KcYvXDG&{P56QnWVSS zs*U+GO0Xqy18ixHUft%{srR|cHhGuCgY_ZOjBj$pi3&~_Ry;^j*jT=Md-LK*N*46A zOm_b=JmAykII&4%9uwMA&s*xoDi|ms8JibMMA?!u>XegBY&V2p789$G|Ee+$3N{kD zaj!Wcg-n&r_bDTkp7ap~s}v@jv~LJfkX+pe)rFFsznmfp;5dcwc$L0Qq2M1K)vZt! zgk=Vj9l+Ux9*OQyk1J{OQT?G;iB-X^ia0ON0s4 zi;~^cbuBvBnXOfCnTh>feeVym{4_?&kVfAF6}aDVxANgP=$huwTYhpOZPniEdf<@OCOjkG?t*9;z(w0J?6Q-G?l*AYuH z_VAWqgqAxvYe6Vsru_E(nMX(GW^H!L&IQ5Tyo8xi;KmIut@iLOW`6T6J{?OyMg^3{ zAn~L^%!rX7|L(Iuc67yP8~fsj1q0!ui(A6Gs|D=$2J&yWoC~PBUh{$JwIhR}%{YCL z+rIPYh`ak`M##B?BY+sVa#jvrnuX8IFN(&W+@28jj^q%0DAJ?A>L48A>)+jCxMWZr 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 literal 0 HcmV?d00001 diff --git a/tests/test-content/content/public/assets/favicons/apple-touch-icon.png b/tests/test-content/content/public/assets/favicons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..74f72ee94b21ebdee1b9960f4433b9d80c66bb66 GIT binary patch literal 5015 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 MjjkCsBHR=I3$$)5Jpcdz literal 0 HcmV?d00001 diff --git a/tests/test-content/content/public/assets/favicons/favicon-16x16.png b/tests/test-content/content/public/assets/favicons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..26c846932f3e62845d07f23a7c3fc066a115c77e GIT binary patch literal 571 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jw$#(bF~mYJ z`42w}j|hX>Z!pm>zB}mzVsqAQ;2GO44!!2~!W9P79K#y=AAUX`&r@6NZ=_>U+1Z$SSB@>CqNSbMCSH2O!(if4{KeLp?*J&`JYD@<);T3K0RS&C(trQ} literal 0 HcmV?d00001 diff --git a/tests/test-content/content/public/assets/favicons/favicon-32x32.png b/tests/test-content/content/public/assets/favicons/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..bbb0aba46cd2af783fc8fff8f9c545891211ec82 GIT binary patch literal 922 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZp5{&f@9f7!tvm z{D+@~hnZsr+l9OhS9Na46g)i1-8MCS{o$oNsj3o(dQ2YH2qeGT$jwt8x8GgR`Pqie z2F542J{jIvb;58(C#RzK!VMd(9(-Hl*0 z-a`Eot&V;j^3Ia6ixU2?ZRg1go@RWf`ZMEU#t3HyXHh|>e^psLMofDWrrQ7ilWOt# zh2`N^b^V7{@%2BC1?(s@KYZ*TNEY5Fq2C=#QyJ(qXjR{Ik(-?ZI}4x$Q>nDX4dAZ_>QMX z<65wxAltd9<}{P0?1c(vFUUXY+Ul?zqFEr^nuA^P-af}TFB`S0iV_w$&??@OIg|3~StU4kbElYQO)M34Aw zy$=HtWtt~-FhulZDKeaTZ*<@kd)ri37RfzZLM6gf*$r72tW&m4bK z_5;2y$qlEPDvD!8Ehtx|OgxYHD5 z&C^iB>D9vR7n5|O;-#36gikfoWj3CGvy6eC%@3S8;;qdTTt8{4T?}vRC5AI*p$V6B zObx;dHg7!CvSB;B120#~fg>*atQ|`m_Vrj8XshWI#YUWMus$Fd5g1eTi_1B1#ze7Z6?^_Ri>)jVR(7*` zI|Yjhr(}HGx_lZ?VPks(hq-ze6Thx%Tc=_k57QhOjYn6W9gr2sSa5KGlGYIg)dPnU p)@z+#psMJUyL-c|;}(yqk25frob{C0%E$rA7oM(uF6*2UngD1HZgl_v literal 0 HcmV?d00001 diff --git a/tests/test-content/content/public/assets/favicons/favicon.ico b/tests/test-content/content/public/assets/favicons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7a22a60225624ff223dbc6fb051d9199ffed78be GIT binary patch literal 15406 zcmeHOiEkA}7+<4*0D|Ccsw9dU6f{aSnjjvDM2#mAM8!x{JQ9OaO1Z>>fJiSWlwOb0 zgSNEg^)AqZUi5%cdQ;j`Xo1$=rO@N`_?&O+Zf39dcK2=HL+K=wnVp?)e!p+7o%hZ6 z80HS#b`-9NYTBEs*5z8Q?M1I4zkske{W_c92+EmQS zhx;tzTBeFg(JI#tVOC9H$|w5<^4{Tl4u=mHnY69lsOV2wYgzh(F) zJI8$2KLjI|0YMvTuu~=Dk9zR!rZ`$<|4JXMPi9bF* z4$0wP3-C|Gt(IWY1$yvn%VMFgyM>S49Su-ll>)JW9}BSYjNjAINW)3;dhqG-Rm%Ig zKR`GoOYp=6M&siii09X?zb}ZPWS@=3kMdhguv4J14XKSu~TWM=c8DyE=_=vyF#G$Mph<{Jl}Vkx5KlNUEp>pM>`2I(K0JYoU)VL4l~~QI`;RqLQ`EPv@{k#XWMz!Zfz0SJ)O@| znL#PBk#XM_pLO@c!M|RHV=+HLLv^~LHc({-_PxnX*~0VZ^uW|_^KA0{-T;;%eRd1` zSgDsrdkD>2oX<47O6=(BTc7-zaluQh^55+`0}4;KJgH)KBjG;>mpSBy)!Df6Pbi zTg=|gadmIabQ~J1)5zZQn1ny>M{3XP&eRR>2uy!2knqPbXt`LZNNs#<6h=pfHN~+} z3!t3QAudR0C*tPP`E&7#Q1z85R)2$VzeVUYJHZ`t$lW#G7gm=>K`XIIU?12U(xSeX z?f=S>aK)zaTgU0CHO2kGcm2cQ@9`wN&Kk%Zwr8(Eo1h$r3UU{q6zt(I#=<=?Gky)Z z|L#(Pvs&(K{+vDup`WX=afp?&t(w<_k^GA?cEdn_ry!zV0p8u9o$=K1 zo$Q&{o1A^Qey3BkJ9gW!F;8aym*)A4o0mM}-7>Bpv`N$4xqL;@L5YmD0weoh4S(c< zcHRh|*Jb%99$I0IuH>CF_#=s;Oix+<;Y2Sj!CCyP&qT@c7yCik(#2W)@jJ}Tc{;^2 zzLww&{`jpz-z%E;M1BUG%|D=pJ~)~``dS>1{Tbx;(4g(}Rozq23L42jCutM3wVad0 ztCxxa&x09pzYrh7`Ml2=-T&x^cIx;Jz;mjWU?hKKl7C##Cs1%~ z2OqJ2Gva@R=>6}oex~9zaU|daz_pF{m3o7b{Ke-Vv)ueutgk@V-j^%sHK1553*ZPUnyQ7cC9> zd~B}Ih5E`Q$WB}j`+uJ!S-0Fe#_xvSVC4Ma_-IK3w$24LzBNkvIg>x`o8^V3|D65M z+wDFGB)74>OVW+m^iBc(awo;B`# Date: Wed, 10 Nov 2021 17:05:19 -0600 Subject: [PATCH 7/7] caching and cleaning - probably not necessary really --- src/Content/Markdown.php | 9 +--- src/Documents/Sitemap.php | 2 +- src/File.php | 48 ++++++++++------- src/HttpRequest.php | 56 +++++++++----------- src/HttpResponse.php | 2 +- src/PageComponents/OriginalContentNotice.php | 1 - src/ServerGlobals.php | 24 --------- 7 files changed, 58 insertions(+), 84 deletions(-) diff --git a/src/Content/Markdown.php b/src/Content/Markdown.php index 9b05d1a7..5213a6e3 100644 --- a/src/Content/Markdown.php +++ b/src/Content/Markdown.php @@ -137,9 +137,9 @@ public function pageTitle(): string return implode(' | ', $titles); } - public function canonicalURl(): string + public function file(): File { - return $this->file()->canonicalUrl(); + return $this->file; } private function fileContent(): string @@ -150,11 +150,6 @@ private function fileContent(): string return $this->fileContent; } - private function file(): File - { - return $this->file; - } - private function fileSystem(): FileSystemInterface { return $this->fileSystem; diff --git a/src/Documents/Sitemap.php b/src/Documents/Sitemap.php index 2c72919e..f46529d5 100644 --- a/src/Documents/Sitemap.php +++ b/src/Documents/Sitemap.php @@ -31,7 +31,7 @@ public static function create(FileSystemInterface $fileSystem): string $urls = []; foreach ($markdown as $m) { $urls[] = Element::url( - Element::loc($m->canonicalUrl()) + Element::loc($m->file()->canonicalUrl()) ); } diff --git a/src/File.php b/src/File.php index a5656318..536e0d46 100644 --- a/src/File.php +++ b/src/File.php @@ -12,6 +12,10 @@ class File { private string $contentFileName = '/content.md'; + private string $contents = ''; + + private string $mimetype = ''; + public static function at(string $localPath, FileSystemInterface $in): File { return new File($localPath, $in); @@ -86,34 +90,40 @@ public function up(): File public function contents(): string { - $contents = file_get_contents($this->path()); - if ($contents === false) { - return ''; + if (strlen($this->contents) === 0) { + $contents = file_get_contents($this->path()); + if ($contents === false) { + return ''; + } + $this->contents = $contents; } - return $contents; + return $this->contents; } public function mimetype(): string { - $type = mime_content_type($this->path()); - if (is_bool($type) and $type === false) { - return ''; - } + if (strlen($this->mimetype) === 0) { + $type = mime_content_type($this->path()); + if (is_bool($type) and $type === false) { + return ''; + } - if ($type === 'text/plain') { - $extensionMap = [ - 'md' => 'text/html', - 'css' => 'text/css', - 'js' => 'text/javascript', - 'xml' => 'application/xml' - ]; + if ($type === 'text/plain') { + $extensionMap = [ + 'md' => 'text/html', + 'css' => 'text/css', + 'js' => 'text/javascript', + 'xml' => 'application/xml' + ]; - $parts = explode('.', $this->path()); - $extension = array_pop($parts); + $parts = explode('.', $this->path()); + $extension = array_pop($parts); - $type = $extensionMap[$extension]; + $type = $extensionMap[$extension]; + } + $this->mimetype = $type; } - return $type; + return $this->mimetype; } public function canonicalUrl(): string diff --git a/src/HttpRequest.php b/src/HttpRequest.php index ff8b0415..f802f38b 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -27,6 +27,10 @@ class HttpRequest private string $localPath = ''; + private File $localFile; + + private string $possibleFileName = ''; + public static function with( ServerGlobals $serverGlobals, FileSystemInterface $in @@ -34,20 +38,6 @@ public static function with( return new HttpRequest($serverGlobals, $in); } -// public static function fromGlobals(): HttpRequest -// { -// if ($fileSystem === null) { -// $fileSystem = new FileSystemInterface(); -// } -// -// return new HttpRequest(); -// } -// -// // public static function for(FileSystemInterface $contentFolder): HttpRequest -// // { -// // return new HttpRequest($contentFolder, $serverGlobals); -// // } -// private function __construct( private ServerGlobals $serverGlobals, private FileSystemInterface $fileSystem @@ -84,7 +74,13 @@ public function isNotFound(): bool public function localFile(): File { - return File::at(localPath: $this->localPath(), in: $this->fileSystem()); + if (! isset($this->localFile)) { + $this->localFile = File::at( + localPath: $this->localPath(), + in: $this->fileSystem() + ); + } + return $this->localFile; } public function isFile(): bool @@ -110,9 +106,8 @@ public function fileSystem(): FileSystemInterface private function localPath(): string { if (empty($this->localPath)) { - $possibleFileName = $this->possibleFileName(); $relativePath = $this->psrPath(); - if (empty($possibleFileName)) { + if (empty($this->possibleFileName())) { $relativePath = $this->psrPath() . '/content.md'; } @@ -129,16 +124,20 @@ private function localPath(): string private function possibleFileName(): string { - $parts = explode('/', $this->psrPath()); - $lastPart = array_slice($parts, -1); - $possibleFileName = array_shift($lastPart); - if ( - $possibleFileName === null or - ! str_contains($possibleFileName, '.') - ) { - return ''; + if (strlen($this->possibleFileName) === 0) { + $parts = explode('/', $this->psrPath()); + $lastPart = array_slice($parts, -1); + $possibleFileName = array_shift($lastPart); + if ( + $possibleFileName === null or + ! str_contains($possibleFileName, '.') + ) { + return ''; + } + + $this->possibleFileName = $possibleFileName; } - return $possibleFileName; + return $this->possibleFileName; } /** @@ -178,9 +177,4 @@ private function psrPath(): string } return $psrPath; } -// -// private function uri(): UriInterface -// { -// return $this->psrRequest()->getUri(); -// } } diff --git a/src/HttpResponse.php b/src/HttpResponse.php index f40e74dd..3a753f64 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -83,7 +83,7 @@ public function body(): string } elseif ($this->statusCode() === 405) { $localPath = $this->request()->fileSystem()->publicRoot() . - '/public/error-405.md'; + '/error-405.md'; $localFile = File::at($localPath, $this->request()->fileSystem()); } diff --git a/src/PageComponents/OriginalContentNotice.php b/src/PageComponents/OriginalContentNotice.php index bcd612c9..01a01c30 100644 --- a/src/PageComponents/OriginalContentNotice.php +++ b/src/PageComponents/OriginalContentNotice.php @@ -18,7 +18,6 @@ public static function create( FrontMatter $frontMatter, FileSystemInterface $fileSystem ): string { - // $contentRoot = FileSystem::contentRoot(); $contentRoot = $fileSystem->contentRoot(); $noticesRoot = $contentRoot . '/notices'; diff --git a/src/ServerGlobals.php b/src/ServerGlobals.php index aa2102c3..980c1474 100644 --- a/src/ServerGlobals.php +++ b/src/ServerGlobals.php @@ -61,11 +61,6 @@ private function hasRequiredValues(): bool array_key_exists('APP_URL', $globals); } -// public function appEnvIsNot(string $value): bool -// { -// return $this->appEnv() !== $value; -// } -// public function appEnv(): string { if ($this->hasRequiredValues()) { @@ -75,25 +70,6 @@ public function appEnv(): string return ''; } -// public function isMissingAppEnv(): bool -// { -// return ! $this->hasAppEnv(); -// } -// -// public function isMissingAppUrl(): bool -// { -// return ! $this->hasAppUrl(); -// } -// -// public function appUrl(): string -// { -// if ($this->hasAppUrl()) { -// $globals = $this->globals(); -// return strval($globals['APP_URL']); -// } -// return ''; -// } - /** * @return array */