├── tests ├── test_project │ ├── static │ │ ├── file.html │ │ └── sub-dir │ │ │ └── test.html │ ├── .env │ ├── resources │ │ ├── js │ │ │ └── main.js │ │ ├── css │ │ │ ├── normal.css │ │ │ └── scss_file.scss │ │ ├── images │ │ │ ├── green.jpg │ │ │ ├── purple.jpg │ │ │ ├── teal.jpg │ │ │ └── green_large.jpg │ │ └── view │ │ │ ├── _partials │ │ │ └── main.twig │ │ │ ├── detail.twig │ │ │ ├── index.twig │ │ │ └── overview.twig │ ├── config │ │ ├── testPlugin │ │ │ ├── config.php │ │ │ └── services.yaml │ │ ├── config.php │ │ └── config_real.php │ ├── src │ │ ├── routes.php │ │ ├── entries.yaml │ │ ├── Plugin │ │ │ ├── TestPluginService.php │ │ │ └── TestPlugin.php │ │ ├── Controller │ │ │ └── MyController.php │ │ └── site.yaml │ └── public │ │ └── index.php ├── bootstrap.php ├── Stitcher │ ├── Template │ │ ├── RendererFactoryTest.php │ │ └── TwigRendererTest.php │ ├── Variable │ │ ├── JsonVariableTest.php │ │ ├── MarkdownVariableTest.php │ │ ├── YamlVariableTest.php │ │ ├── ImageVariableTest.php │ │ ├── VariableFactoryTest.php │ │ └── VariableParserTest.php │ ├── Application │ │ ├── RouterTest.php │ │ ├── ProductionServerTest.php │ │ └── DevelopmentServerTest.php │ ├── Integration │ │ ├── StaticFilesTest.php │ │ ├── ConfigTest.php │ │ ├── BasicMetaTest.php │ │ ├── PluginTest.php │ │ ├── PaginatedMetaTest.php │ │ ├── DevelopmentServerTest.php │ │ ├── ProductionServerTest.php │ │ ├── CollectionMetaTest.php │ │ └── FullSiteParseTest.php │ ├── Page │ │ ├── Adapter │ │ │ ├── AdapterFactoryTest.php │ │ │ ├── OrderAdapterTest.php │ │ │ ├── FilterAdapterTest.php │ │ │ ├── CollectionAdapterTest.php │ │ │ └── PaginationAdapterTest.php │ │ ├── PageRendererTest.php │ │ ├── PageTest.php │ │ ├── PageFactoryTest.php │ │ └── PageParserTest.php │ ├── Task │ │ ├── ParseTest.php │ │ └── PartialParseTest.php │ ├── Renderer │ │ └── Extension │ │ │ ├── CssTest.php │ │ │ └── JsTest.php │ └── AppTest.php ├── SetUp.php ├── Pageon │ ├── Html │ │ ├── SiteMapTest.php │ │ ├── Meta │ │ │ ├── Item │ │ │ │ ├── CharsetMetaTest.php │ │ │ │ ├── LinkMetaTest.php │ │ │ │ ├── HttpEquivMetaTest.php │ │ │ │ ├── NameMetaTest.php │ │ │ │ ├── ItemPropMetaTest.php │ │ │ │ └── PropertyMetaTest.php │ │ │ ├── Social │ │ │ │ ├── GooglePlusMetaTest.php │ │ │ │ ├── TwitterMetaTest.php │ │ │ │ └── OpenGraphMetaTest.php │ │ │ └── MetaTest.php │ │ └── Image │ │ │ ├── ImageTest.php │ │ │ └── ImageFactoryTest.php │ └── Lib │ │ └── MarkdownParserTest.php ├── CreateStitcherFiles.php ├── StitcherTest.php ├── StitcherTestBootstrap.php └── CreateStitcherObjects.php ├── .gitignore ├── src ├── Stitcher │ ├── Task.php │ ├── Renderer │ │ ├── Extension.php │ │ ├── Renderer.php │ │ ├── Extension │ │ │ ├── Page.php │ │ │ ├── Css.php │ │ │ └── Js.php │ │ ├── TwigRenderer.php │ │ └── RendererFactory.php │ ├── Configureable.php │ ├── Page │ │ ├── Adapter.php │ │ ├── PageCollection.php │ │ ├── PageRenderer.php │ │ ├── PageFactory.php │ │ ├── Page.php │ │ ├── Adapter │ │ │ ├── FilterAdapter.php │ │ │ ├── AdapterFactory.php │ │ │ ├── OrderAdapter.php │ │ │ └── PaginationAdapter.php │ │ └── PageParser.php │ ├── Plugin.php │ ├── Exception │ │ ├── InvalidFilterAdapter.php │ │ ├── FileNotFound.php │ │ ├── StitcherException.php │ │ ├── InvalidCollectionAdapter.php │ │ ├── InvalidOrderAdapter.php │ │ ├── InvalidPaginationAdapter.php │ │ ├── Http.php │ │ ├── InvalidPlugin.php │ │ ├── ErrorHandler.php │ │ └── InvalidConfiguration.php │ ├── Variable │ │ ├── DefaultVariable.php │ │ ├── HtmlVariable.php │ │ ├── JsonVariable.php │ │ ├── AbstractVariable.php │ │ ├── VariableParser.php │ │ ├── MarkdownVariable.php │ │ ├── DirectoryVariable.php │ │ ├── ImageVariable.php │ │ ├── YamlVariable.php │ │ └── VariableFactory.php │ ├── DynamicFactory.php │ ├── Task │ │ ├── Parse.php │ │ ├── RenderSiteMap.php │ │ ├── CopyStaticFiles.php │ │ ├── PartialParse.php │ │ └── AbstractParse.php │ ├── Application │ │ ├── ProductionServer.php │ │ ├── DevelopmentServer.php │ │ ├── Router.php │ │ └── Server.php │ ├── File.php │ └── App.php ├── Pageon │ ├── Html │ │ ├── Meta │ │ │ ├── MetaItem.php │ │ │ ├── SocialMeta.php │ │ │ ├── Item │ │ │ │ ├── CharsetMeta.php │ │ │ │ ├── LinkMeta.php │ │ │ │ ├── HttpEquivMeta.php │ │ │ │ ├── NameMeta.php │ │ │ │ ├── PropertyMeta.php │ │ │ │ └── ItemPropMeta.php │ │ │ ├── Social │ │ │ │ ├── GooglePlusMeta.php │ │ │ │ ├── OpenGraphMeta.php │ │ │ │ └── TwitterMeta.php │ │ │ └── Meta.php │ │ ├── Image │ │ │ ├── Scaler.php │ │ │ ├── FixedWidthScaler.php │ │ │ ├── FilesizeScaler.php │ │ │ └── Image.php │ │ ├── Source.php │ │ └── SiteMap.php │ ├── helpers.php │ ├── Http │ │ ├── Header.php │ │ └── HeaderContainer.php │ ├── Lib │ │ └── Markdown │ │ │ ├── ExternalLinkRenderer.php │ │ │ ├── MarkdownParser.php │ │ │ └── ImageRenderer.php │ └── Config.php └── static │ └── exception.html ├── .scrutinizer.yml ├── phpunit.xml ├── CONTRIBUTING.md ├── LICENCE ├── composer.json └── README.md /tests/test_project/static/file.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_project/.env: -------------------------------------------------------------------------------- 1 | TEST_KEY=foo 2 | -------------------------------------------------------------------------------- /tests/test_project/static/sub-dir/test.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_project/resources/js/main.js: -------------------------------------------------------------------------------- 1 | console.log('hi'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | /tests/.cache 5 | /vendor 6 | /test_project 7 | -------------------------------------------------------------------------------- /tests/test_project/resources/css/normal.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_project/resources/css/scss_file.scss: -------------------------------------------------------------------------------- 1 | body { 2 | h1 { 3 | background-color: red; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Stitcher/Task.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'item' => 1, 6 | ], 7 | ]; 8 | -------------------------------------------------------------------------------- /tests/test_project/resources/images/green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pageon/stitcher-core/HEAD/tests/test_project/resources/images/green.jpg -------------------------------------------------------------------------------- /tests/test_project/resources/images/purple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pageon/stitcher-core/HEAD/tests/test_project/resources/images/purple.jpg -------------------------------------------------------------------------------- /tests/test_project/resources/images/teal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pageon/stitcher-core/HEAD/tests/test_project/resources/images/teal.jpg -------------------------------------------------------------------------------- /tests/test_project/resources/images/green_large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pageon/stitcher-core/HEAD/tests/test_project/resources/images/green_large.jpg -------------------------------------------------------------------------------- /src/Stitcher/Renderer/Extension.php: -------------------------------------------------------------------------------- 1 | get('/test/{id}/{name}', \Stitcher\Test\Controller\MyController::class) 5 | ->redirect('/redirect', '/entries') 6 | ; 7 | -------------------------------------------------------------------------------- /tests/test_project/config/testPlugin/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | testPluginService: 4 | class: Stitcher\Test\Plugin\TestPluginService 5 | arguments: 6 | - '%test.plugin.item%' 7 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/MetaItem.php: -------------------------------------------------------------------------------- 1 | 'test', 5 | 'nested' => [ 6 | 'item' => 'bar', 7 | ], 8 | 'with' => [ 9 | 'env' => env('TEST_KEY'), 10 | ], 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/test_project/src/entries.yaml: -------------------------------------------------------------------------------- 1 | a: 2 | title: A 3 | image: 4 | src: resources/images/green.jpg 5 | alt: test 6 | b: 7 | title: B 8 | c: 9 | title: C 10 | d: 11 | title: D 12 | e: 13 | title: E 14 | -------------------------------------------------------------------------------- /src/Pageon/Html/Image/Scaler.php: -------------------------------------------------------------------------------- 1 | item = $item; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Stitcher/Plugin.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if _meta is defined %} 4 | {{ _meta.render()|raw }} 5 | {% endif %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | 10 | {% block content %}{% endblock %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Pageon/helpers.php: -------------------------------------------------------------------------------- 1 | run()); 16 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/SocialMeta.php: -------------------------------------------------------------------------------- 1 | parsed = $this->unparsed; 15 | 16 | return $this; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/test_project/resources/view/detail.twig: -------------------------------------------------------------------------------- 1 | {% extends '_partials/main.twig' %} 2 | 3 | {% block content %} 4 | 5 |
6 |

{{ entry.title }}

7 | 8 | {% if entry.image is defined %} 9 | {{ entry.image.alt }} 12 | {% endif %} 13 |
14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tests/test_project/resources/view/index.twig: -------------------------------------------------------------------------------- 1 | {% extends '_partials/main.twig' %} 2 | 3 | {% block head %} 4 | {{ css.link('/resources/css/scss_file.scss')|raw }} 5 | {{ css.inline('/resources/css/normal.css')|raw }} 6 | 7 | {{ js.defer().link('/resources/js/main.js')|raw }} 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {% if variable is defined %} 12 | {{ variable }} 13 | {% endif %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /src/Stitcher/DynamicFactory.php: -------------------------------------------------------------------------------- 1 | rules[$class] = $callback; 12 | 13 | return $this; 14 | } 15 | 16 | protected function getRules(): array 17 | { 18 | return $this->rules; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/HtmlVariable.php: -------------------------------------------------------------------------------- 1 | parsed = File::read($this->unparsed); 12 | 13 | if ($this->parsed === null) { 14 | $this->parsed = $this->unparsed; 15 | } 16 | 17 | return $this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/JsonVariable.php: -------------------------------------------------------------------------------- 1 | parsed = json_decode(File::read($this->unparsed), true); 17 | 18 | return $this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 4 | version: 7.1 5 | tests: 6 | override: 7 | - 8 | command: 'vendor/bin/phpunit --coverage-clover=coverage.xml' 9 | coverage: 10 | file: 'coverage.xml' 11 | format: 'clover' 12 | checks: 13 | php: 14 | code_rating: true 15 | duplication: true 16 | 17 | filter: 18 | excluded_paths: 19 | - "tests/" 20 | -------------------------------------------------------------------------------- /tests/Stitcher/Template/RendererFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(TwigRenderer::class, $factory->create()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Stitcher/Task/Parse.php: -------------------------------------------------------------------------------- 1 | configurationFile)); 13 | 14 | $pages = $this->parsePageConfiguration($parsedConfiguration); 15 | 16 | $this->renderPages($pages); 17 | 18 | $this->executeSubTasks(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Pageon/Html/Source.php: -------------------------------------------------------------------------------- 1 | url = $url; 13 | $this->content = $content; 14 | } 15 | 16 | public function url(): string 17 | { 18 | return $this->url; 19 | } 20 | 21 | public function content(): string 22 | { 23 | return $this->content; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_project/config/config_real.php: -------------------------------------------------------------------------------- 1 | [ 7 | \Stitcher\Test\Plugin\TestPlugin::class, 8 | ], 9 | 10 | 'publicDirectory' => File::path('public'), 11 | 'sourceDirectory' => File::path('src'), 12 | 'templateDirectory' => File::path('resources/view'), 13 | 'configurationFile' => File::path('src/site.yaml'), 14 | 15 | 'staticFiles' => [ 16 | 'static/file.html', 17 | 'static/sub-dir/', 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /src/Stitcher/Exception/FileNotFound.php: -------------------------------------------------------------------------------- 1 | mirror( 17 | __DIR__. '/' . self::TEST_PROJECT, 18 | __DIR__ . '/../' . self::TEST_PROJECT 19 | ); 20 | 21 | File::base(__DIR__ . '/../' . self::TEST_PROJECT); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Stitcher is a very young open source project, but you're always welcome to make contributions. 4 | 5 | When submitting a contribution, you should always create a pull request with the `develop` branch. 6 | Furthermore you should also search for and mention related issues your pull request targets. 7 | 8 | Writing unit tests is highly recommended. Following PSR-2 guidelines is required. 9 | Note that some code in this repository is not PSR-2 compliant yet. 10 | You're always free to make the necessary changes to match PSR-2. 11 | -------------------------------------------------------------------------------- /src/Pageon/Http/Header.php: -------------------------------------------------------------------------------- 1 | name = $name; 14 | $this->content = $content; 15 | } 16 | 17 | public static function make(string $name, ?string $content = null): Header 18 | { 19 | return new self($name, $content); 20 | } 21 | 22 | public function __toString(): string 23 | { 24 | return "{$this->name}: {$this->content}"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Stitcher/Exception/StitcherException.php: -------------------------------------------------------------------------------- 1 | title = $title; 17 | $this->body = $body; 18 | } 19 | 20 | public function title(): string 21 | { 22 | return $this->title; 23 | } 24 | 25 | public function body(): ?string 26 | { 27 | return $this->body; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Stitcher/Variable/JsonVariableTest.php: -------------------------------------------------------------------------------- 1 | 'test', 16 | ])); 17 | 18 | $variable = JsonVariable::make($path)->parse(); 19 | 20 | $this->assertTrue(\is_array($variable->getParsed())); 21 | $this->assertArrayHasKey('test', $variable->getParsed()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Pageon/Html/SiteMapTest.php: -------------------------------------------------------------------------------- 1 | addPath('/blog'); 16 | $siteMap->addPath('/guide'); 17 | 18 | $xml = $siteMap->render(); 19 | 20 | $this->assertContains('stitcher.io/blog', $xml); 21 | $this->assertContains('stitcher.io/guide', $xml); 22 | $this->assertContains('', $xml); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/test_project/src/Plugin/TestPlugin.php: -------------------------------------------------------------------------------- 1 | renderer = $renderer; 14 | } 15 | 16 | public static function make(Renderer $renderer): PageRenderer 17 | { 18 | return new self($renderer); 19 | } 20 | 21 | public function render(Page $page): string 22 | { 23 | $variables = $page->variables(); 24 | $variables['_meta'] = $page->meta(); 25 | 26 | return $this->renderer->renderTemplate($page->template(), $variables); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Stitcher/Application/RouterTest.php: -------------------------------------------------------------------------------- 1 | get('/test/{id}/{name}', MyController::class); 20 | 21 | $response = $router->dispatch(new Request('GET', '/test/1/abc')); 22 | 23 | $this->assertContains('test 1 abc', $response->getBody()->getContents()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Stitcher/Template/TwigRendererTest.php: -------------------------------------------------------------------------------- 1 | createAllTemplates(); 18 | 19 | $html = $renderer->renderTemplate('index.twig', [ 20 | 'variable' => 'hello world' 21 | ]); 22 | 23 | $this->assertContains('hello world', $html); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Stitcher/Exception/InvalidOrderAdapter.php: -------------------------------------------------------------------------------- 1 | publicDirectory = $publicDirectory; 19 | $this->siteMap = $siteMap; 20 | } 21 | 22 | public function execute(): void 23 | { 24 | $siteMap = $this->siteMap->render(); 25 | 26 | file_put_contents($this->publicDirectory . '/sitemap.xml', $siteMap); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/test_project/src/site.yaml: -------------------------------------------------------------------------------- 1 | /entries/page-{page}: 2 | template: overview.twig 3 | variables: 4 | entries: src/entries.yaml 5 | config: 6 | pagination: 7 | variable: entries 8 | perPage: 2 9 | parameter: page 10 | 11 | /entries/{id}: 12 | template: detail.twig 13 | variables: 14 | entry: src/entries.yaml 15 | config: 16 | collection: 17 | variable: entry 18 | parameter: id 19 | 20 | /entries: 21 | template: overview.twig 22 | variables: 23 | entries: src/entries.yaml 24 | 25 | /: 26 | template: index.twig 27 | variables: 28 | title: Hello World 29 | description: Description 30 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Item/CharsetMetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_can_be_rendered(): void 24 | { 25 | $meta = CharsetMeta::create('UTF-16'); 26 | $tag = $meta->render(); 27 | 28 | $this->assertContains('', $tag); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Item/LinkMetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_can_be_rendered(): void 24 | { 25 | $meta = LinkMeta::create('next', '/?page=3'); 26 | $tag = $meta->render(); 27 | 28 | $this->assertContains('', $tag); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/StaticFilesTest.php: -------------------------------------------------------------------------------- 1 | execute(); 19 | 20 | $fs = new Filesystem(); 21 | 22 | $this->assertTrue($fs->exists(File::path('public/static/file.html'))); 23 | $this->assertTrue($fs->exists(File::path('public/static/sub-dir/test.html'))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/AbstractVariable.php: -------------------------------------------------------------------------------- 1 | unparsed = $unparsed; 15 | } 16 | 17 | /** 18 | * @return mixed 19 | */ 20 | public function getUnparsed() 21 | { 22 | return $this->unparsed; 23 | } 24 | 25 | /** 26 | * @return mixed 27 | */ 28 | public function getParsed() 29 | { 30 | if (! $this->parsed) { 31 | $this->parse(); 32 | } 33 | 34 | return $this->parsed; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Item/HttpEquivMetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_can_be_rendered(): void 24 | { 25 | $meta = HttpEquivMeta::create('Expires', '3000'); 26 | $tag = $meta->render(); 27 | 28 | $this->assertContains('', $tag); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/test_project/resources/view/overview.twig: -------------------------------------------------------------------------------- 1 | {% extends '_partials/main.twig' %} 2 | 3 | {% block content %} 4 | 5 |
6 | {% for entry in entries %} 7 |

{{ entry.title }}

8 | {% endfor %} 9 |
10 | 11 |
12 | {% if _pagination is defined %} 13 | 14 | {% if _pagination.previous != null %} 15 | 16 | {{ _pagination.previous.index }} 17 | 18 | {% endif %} 19 | 20 | {% if _pagination.next != null %} 21 | 22 | {{ _pagination.next.index }} 23 | 24 | {% endif %} 25 | 26 | {% endif %} 27 |
28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Item/CharsetMeta.php: -------------------------------------------------------------------------------- 1 | charset}\">"; 28 | } 29 | 30 | /** 31 | * CharsetMeta constructor. 32 | * 33 | * @param string $charset 34 | */ 35 | public function __construct(string $charset) { 36 | $this->charset = $charset; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Social/GooglePlusMeta.php: -------------------------------------------------------------------------------- 1 | meta = $meta; 14 | } 15 | 16 | public function title(string $title) : SocialMeta { 17 | $this->meta->itemprop('name', $title); 18 | 19 | return $this; 20 | } 21 | 22 | public function description(string $description) : SocialMeta { 23 | $this->meta->itemprop('description', $description); 24 | 25 | return $this; 26 | } 27 | 28 | public function image(string $image) : SocialMeta { 29 | $this->meta->itemprop('image', $image); 30 | 31 | return $this; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Image/ImageTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Image::class, $image); 16 | } 17 | 18 | /** @test */ 19 | public function it_can_be_made_with_sizes(): void 20 | { 21 | $image = Image::make('resources/green.jpg', '100vw'); 22 | 23 | $this->assertEquals('100vw', $image->sizes()); 24 | } 25 | 26 | /** @test */ 27 | public function it_can_be_made_with_alt(): void 28 | { 29 | $image = Image::make('resources/green.jpg', null, 'alt'); 30 | 31 | $this->assertEquals('alt', $image->alt()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Stitcher/Variable/MarkdownVariableTest.php: -------------------------------------------------------------------------------- 1 | getMarkdown()); 18 | 19 | $variable = MarkdownVariable::make($path, $this->createMarkdownParser())->parse(); 20 | 21 | $this->assertTrue(\is_string($variable->getParsed())); 22 | $this->assertContains('

', $variable->getParsed()); 23 | } 24 | 25 | private function getMarkdown() : string 26 | { 27 | return <<meta = $meta; 14 | 15 | $this->meta->property('og:type', $type); 16 | } 17 | 18 | public function title(string $title) : SocialMeta { 19 | $this->meta->property('og:title', $title); 20 | 21 | return $this; 22 | } 23 | 24 | public function description(string $description) : SocialMeta { 25 | $this->meta->property('og:description', $description); 26 | 27 | return $this; 28 | } 29 | 30 | public function image(string $image) : SocialMeta { 31 | $this->meta->property('og:image', $image); 32 | 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Pageon/Http/HeaderContainer.php: -------------------------------------------------------------------------------- 1 | position = 0; 16 | $this->headers[] = $header; 17 | 18 | return $this; 19 | } 20 | 21 | public function current(): Header 22 | { 23 | return $this->headers[$this->position]; 24 | } 25 | 26 | public function next(): void 27 | { 28 | ++$this->position; 29 | } 30 | 31 | public function key(): int 32 | { 33 | return $this->position; 34 | } 35 | 36 | public function valid(): bool 37 | { 38 | return isset($this->headers[$this->position]); 39 | } 40 | 41 | public function rewind(): void 42 | { 43 | $this->position = 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Pageon/Html/Image/FixedWidthScaler.php: -------------------------------------------------------------------------------- 1 | fixedWidths = $fixedWidths; 14 | } 15 | 16 | public static function make(array $fixedWidths): FixedWidthScaler 17 | { 18 | return new self($fixedWidths); 19 | } 20 | 21 | public function getVariations(ScaleableImage $scaleableImage): array 22 | { 23 | $width = $scaleableImage->getWidth(); 24 | $height = $scaleableImage->getHeight(); 25 | $ratio = $width / $height; 26 | $variations = []; 27 | 28 | foreach ($this->fixedWidths as $fixedWidth) { 29 | $variations[$fixedWidth] = round($fixedWidth * $ratio); 30 | } 31 | 32 | return $variations; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Stitcher/Application/ProductionServer.php: -------------------------------------------------------------------------------- 1 | rootDirectory = $rootDirectory; 15 | } 16 | 17 | public static function make(string $rootDirectory): ProductionServer 18 | { 19 | return new self($rootDirectory); 20 | } 21 | 22 | protected function handleStaticRoute(): ?Response 23 | { 24 | $path = $this->getCurrentPath(); 25 | 26 | $filename = ltrim($path === '/' ? 'index.html' : "{$path}.html", '/'); 27 | 28 | $body = @file_get_contents("{$this->rootDirectory}/{$filename}"); 29 | 30 | if (!$body) { 31 | return null; 32 | } 33 | 34 | return new Response(200, [], $body); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Social/TwitterMeta.php: -------------------------------------------------------------------------------- 1 | meta = $meta; 14 | 15 | $this->meta->name('twitter:card', $type); 16 | } 17 | 18 | public function title(string $title) : SocialMeta { 19 | $this->meta->name('twitter:title', $title); 20 | 21 | return $this; 22 | } 23 | 24 | public function description(string $description) : SocialMeta { 25 | $this->meta->name('twitter:description', substr($description, 0, 199)); 26 | 27 | return $this; 28 | } 29 | 30 | public function image(string $image) : SocialMeta { 31 | $this->meta->name('twitter:image', $image); 32 | 33 | return $this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/VariableParser.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 12 | $this->factory->setVariableParser($this); 13 | } 14 | 15 | public static function make(VariableFactory $factory): VariableParser 16 | { 17 | return new self($factory); 18 | } 19 | 20 | public function parse($unparsedValue) 21 | { 22 | $variable = $this->factory->create($unparsedValue); 23 | 24 | if ($variable) { 25 | $parsedValue = $variable->getParsed(); 26 | } else { 27 | $parsedValue = $unparsedValue; 28 | } 29 | 30 | return $parsedValue; 31 | } 32 | 33 | public function getVariable($unparsedValue): AbstractVariable 34 | { 35 | return $this->factory->create($unparsedValue); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Pageon/Lib/MarkdownParserTest.php: -------------------------------------------------------------------------------- 1 | parser = $this->createMarkdownParser(); 20 | } 21 | 22 | /** @test */ 23 | public function target_blank_links() 24 | { 25 | $html = $this->parser->parse('[test](*https://stitcher.io)'); 26 | 27 | $this->assertContains('target="_blank"', $html); 28 | } 29 | 30 | /** @test */ 31 | public function images_are_parsed_with_the_image_parser() 32 | { 33 | $html = $this->parser->parse('![test](resources/images/green.jpg)'); 34 | 35 | $this->assertContains('srcset', $html); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/Adapter/AdapterFactoryTest.php: -------------------------------------------------------------------------------- 1 | createVariableParser()); 16 | 17 | $this->assertInstanceOf(CollectionAdapter::class, $factory->create('collection', ['variable' => 'test', 'parameter' => 'id'])); 18 | $this->assertInstanceOf(FilterAdapter::class, $factory->create('filter', ['entries' => ['name' => 'A']])); 19 | $this->assertInstanceOf(PaginationAdapter::class, $factory->create('pagination', ['variable' => 'entries', 'parameter' => 'page'])); 20 | $this->assertInstanceOf(OrderAdapter::class, $factory->create('order', ['variable' => 'entries', 'field' => 'title'])); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Stitcher/Application/ProductionServerTest.php: -------------------------------------------------------------------------------- 1 | parseAll(); 19 | 20 | $server = ProductionServer::make(File::path('public')); 21 | 22 | $html = $server->run(); 23 | 24 | $this->assertContains('', $html); 25 | } 26 | 27 | /** @test */ 28 | public function it_serves_static_html_from_index(): void 29 | { 30 | $this->parseAll(); 31 | 32 | $server = ProductionServer::make(File::path('public')); 33 | 34 | $html = $server->run(); 35 | 36 | $this->assertContains('', $html); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/PageRendererTest.php: -------------------------------------------------------------------------------- 1 | createAllTemplates(); 18 | 19 | $variableParser = $this->createVariableParser(); 20 | $parser = $this->createPageParser($variableParser); 21 | $result = $parser->parse([ 22 | 'id' => '/', 23 | 'template' => 'index.twig', 24 | 'variables' => [ 25 | 'variable' => 'Hello world', 26 | ], 27 | ]); 28 | 29 | $renderer = $this->createPageRenderer(); 30 | $html = $renderer->render($result->first()); 31 | 32 | $this->assertContains('Hello world', $html); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/MarkdownVariable.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 18 | } 19 | 20 | public static function make( 21 | string $value, 22 | MarkdownParser $parser 23 | ): MarkdownVariable { 24 | return new self($value, $parser); 25 | } 26 | 27 | public function parse(): AbstractVariable 28 | { 29 | $contents = File::read($this->unparsed); 30 | 31 | if (! $contents) { 32 | throw InvalidConfiguration::fileNotFound($this->unparsed); 33 | } 34 | 35 | $this->parsed = $this->parser->parse($contents); 36 | 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Stitcher/Variable/YamlVariableTest.php: -------------------------------------------------------------------------------- 1 | createVariableParser())->parse(); 28 | 29 | $this->assertTrue(\is_array($variable->getParsed())); 30 | $this->assertTrue(isset($variable->getParsed()['root']['entry'])); 31 | $this->assertTrue(isset($variable->getParsed()['root']['id'])); 32 | $this->assertEquals('root', $variable->getParsed()['root']['id']); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Stitcher/Renderer/Extension/Page.php: -------------------------------------------------------------------------------- 1 | pageParser = $pageParser; 16 | } 17 | 18 | public function name(): string 19 | { 20 | return '_page'; 21 | } 22 | 23 | public function isActive(string $part): bool 24 | { 25 | return $this->isCurrent($part); 26 | } 27 | 28 | public function isCurrent(string $part): bool 29 | { 30 | $currentPage = $this->pageParser->getCurrentPage(); 31 | 32 | if (!$currentPage) { 33 | return false; 34 | } 35 | 36 | return strpos($currentPage->id(), $part) !== false; 37 | } 38 | 39 | public function current(): ?SitePage 40 | { 41 | return $this->pageParser->getCurrentPage(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/DirectoryVariable.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 17 | } 18 | 19 | public function parse(): AbstractVariable 20 | { 21 | $path = File::path($this->unparsed); 22 | 23 | $files = @scandir($path) ?: []; 24 | 25 | unset($files[0], $files[1]); 26 | 27 | $this->parsed = []; 28 | 29 | foreach ($files as $file) { 30 | $id = pathinfo($file, PATHINFO_FILENAME); 31 | 32 | $filePath = $path . $file; 33 | 34 | $this->parsed[$id] = [ 35 | 'id' => $id, 36 | 'content' => $this->parser->parse($filePath), 37 | ]; 38 | } 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Item/LinkMeta.php: -------------------------------------------------------------------------------- 1 | rel}\" href=\"{$this->href}\">"; 34 | } 35 | 36 | /** 37 | * LinkMeta constructor. 38 | * 39 | * @param string $rel 40 | * @param string $href 41 | */ 42 | public function __construct(string $rel, string $href) { 43 | $this->rel = $rel; 44 | $this->href = $href; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/ImageVariable.php: -------------------------------------------------------------------------------- 1 | imageFactory = $imageFactory; 16 | } 17 | 18 | public static function make( 19 | $attributes, 20 | ImageFactory $imageFactory 21 | ) : ImageVariable { 22 | return new self($attributes, $imageFactory); 23 | } 24 | 25 | public function parse() : AbstractVariable 26 | { 27 | $src = $this->unparsed['src'] ?? $this->unparsed; 28 | $alt = $this->unparsed['alt'] ?? null; 29 | 30 | $image = $this->imageFactory->create($src); 31 | 32 | $this->parsed = [ 33 | 'src' => $image->src(), 34 | 'srcset' => $image->srcset(), 35 | 'alt' => $alt, 36 | ]; 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Stitcher/Exception/Http.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 17 | } 18 | 19 | public static function notFound(string $uri): Http 20 | { 21 | $siteConfigurationFile = File::relativePath(Config::get('configurationFile')); 22 | 23 | $body = <<statusCode; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2017 Pageon VZW 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Pageon/Html/Image/FilesizeScaler.php: -------------------------------------------------------------------------------- 1 | stepModifier = $stopModifier; 14 | } 15 | 16 | public function getVariations(ScaleableImage $scaleableImage): array 17 | { 18 | $fileSize = $scaleableImage->filesize(); 19 | $width = $scaleableImage->width(); 20 | 21 | $ratio = $scaleableImage->height() / $width; 22 | $area = $width * $width * $ratio; 23 | $pixelPrice = $fileSize / $area; 24 | 25 | $stepAmount = $fileSize * $this->stepModifier; 26 | 27 | $variations = []; 28 | 29 | do { 30 | $newWidth = (int) floor(sqrt(($fileSize / $pixelPrice) / $ratio)); 31 | 32 | $variations[$newWidth] = $newWidth * $ratio; 33 | 34 | $fileSize -= $stepAmount; 35 | } while ($fileSize > 0); 36 | 37 | return $variations; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Stitcher/Exception/InvalidPlugin.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Something went wrong 4 | 40 | 41 | 42 |

43 | {{ title }} 44 |

45 | 46 | {{ body }} 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Item/NameMetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_can_be_rendered(): void 24 | { 25 | $meta = NameMeta::create('title', 'Hello World'); 26 | $tag = $meta->render(); 27 | 28 | $this->assertContains('', $tag); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function it_escapes_special_characters(): void 35 | { 36 | $meta = NameMeta::create('title', '""'); 37 | $tag = $meta->render(); 38 | 39 | $this->assertContains('"', $tag); 40 | $this->assertContains('>', $tag); 41 | $this->assertContains('<', $tag); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Item/HttpEquivMeta.php: -------------------------------------------------------------------------------- 1 | httpEquiv}\" content=\"{$this->content}\">"; 34 | } 35 | 36 | /** 37 | * HttpEquivMeta constructor. 38 | * 39 | * @param string $httpEquiv 40 | * @param string $content 41 | */ 42 | public function __construct(string $httpEquiv, string $content) { 43 | $this->httpEquiv = $httpEquiv; 44 | $this->content = $content; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Item/ItemPropMetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_can_be_rendered(): void 24 | { 25 | $meta = ItemPropMeta::create('title', 'Hello World'); 26 | $tag = $meta->render(); 27 | 28 | $this->assertContains('', $tag); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function it_escapes_special_characters(): void 35 | { 36 | $meta = ItemPropMeta::create('title', '""'); 37 | $tag = $meta->render(); 38 | 39 | $this->assertContains('"', $tag); 40 | $this->assertContains('>', $tag); 41 | $this->assertContains('<', $tag); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Item/PropertyMetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_can_be_rendered(): void 24 | { 25 | $meta = PropertyMeta::create('title', 'Hello World'); 26 | $tag = $meta->render(); 27 | 28 | $this->assertContains('', $tag); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function it_escapes_special_characters(): void 35 | { 36 | $meta = PropertyMeta::create('title', '""'); 37 | $tag = $meta->render(); 38 | 39 | $this->assertContains('"', $tag); 40 | $this->assertContains('>', $tag); 41 | $this->assertContains('<', $tag); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Pageon/Lib/Markdown/ExternalLinkRenderer.php: -------------------------------------------------------------------------------- 1 | getUrl(); 23 | 24 | if (strpos($url, '*') === 0) { 25 | $url = substr($url, 1); 26 | 27 | $attributes['target'] = '_blank'; 28 | } 29 | 30 | $attributes['href'] = $url; 31 | 32 | return new HtmlElement( 33 | 'a', 34 | $attributes, 35 | $childRenderer->renderNodes($node->children()) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Stitcher/Page/PageFactory.php: -------------------------------------------------------------------------------- 1 | variableParser = $variableParser; 15 | } 16 | 17 | public static function make(VariableParser $variableParser): PageFactory 18 | { 19 | return new self($variableParser); 20 | } 21 | 22 | public function create($value): Page 23 | { 24 | $id = $value['id'] ?? null; 25 | 26 | if (! $id) { 27 | throw InvalidConfiguration::pageIdMissing($value); 28 | } 29 | 30 | $template = $value['template'] ?? null; 31 | $variables = $value['variables'] ?? []; 32 | 33 | if (! $template) { 34 | throw InvalidConfiguration::pageTemplateMissing($id); 35 | } 36 | 37 | foreach ($variables as $key => $variable) { 38 | $variables[$key] = $this->variableParser->parse($variable); 39 | } 40 | 41 | return Page::make($id, $template, $variables); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/CreateStitcherFiles.php: -------------------------------------------------------------------------------- 1 | createAllTemplates(); 35 | $this->createSiteConfiguration($configurationFile); 36 | $this->createDataFile(); 37 | $this->createImageFiles(); 38 | 39 | $command = Parse::make( 40 | File::path('public'), 41 | $configurationFile, 42 | $this->createPageParser(), 43 | $this->createPageRenderer(), 44 | $this->createSiteMap() 45 | ); 46 | 47 | $command->execute(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Pageon/Html/SiteMap.php: -------------------------------------------------------------------------------- 1 | hostname = trim($hostname, '/'); 13 | } 14 | 15 | public static function make(string $hostname): SiteMap 16 | { 17 | return new self($hostname); 18 | } 19 | 20 | public function addPath(string $path): SiteMap 21 | { 22 | $path = trim($path, '/'); 23 | 24 | $this->urls[] = "{$this->hostname}/{$path}"; 25 | 26 | return $this; 27 | } 28 | 29 | public function render(): string 30 | { 31 | $xml = '' . "\n"; 32 | $xml .= '' . "\n"; 33 | $dateModified = date('c'); 34 | 35 | foreach ($this->urls as $url) { 36 | $xml .= "\t\n"; 37 | $xml .= "\t\t{$url}\n"; 38 | $xml .= "\t\t{$dateModified}\n"; 39 | $xml .= "\t\n"; 40 | } 41 | 42 | $xml .= ''; 43 | 44 | return $xml; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Pageon/Html/Image/Image.php: -------------------------------------------------------------------------------- 1 | src = "/{$src}"; 15 | $this->sizes = $sizes; 16 | $this->alt = $alt; 17 | } 18 | 19 | public static function make(string $src, ?string $sizes = null, ?string $alt = null): Image 20 | { 21 | return new self($src, $sizes, $alt); 22 | } 23 | 24 | public function src(): string 25 | { 26 | return $this->src; 27 | } 28 | 29 | public function srcset(): string 30 | { 31 | return implode(', ', $this->srcset); 32 | } 33 | 34 | public function sizes(): ?string 35 | { 36 | return $this->sizes; 37 | } 38 | 39 | public function alt(): ?string 40 | { 41 | return $this->alt; 42 | } 43 | 44 | public function addSrcset(string $src, int $width): Image 45 | { 46 | $src = ltrim($src, '/'); 47 | 48 | $this->srcset[] = "/{$src} {$width}w"; 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Stitcher/Renderer/TwigRenderer.php: -------------------------------------------------------------------------------- 1 | exists($templateDirectory)) { 17 | $fs->mkdir($templateDirectory); 18 | } 19 | 20 | $loader = new \Twig_Loader_Filesystem($templateDirectory); 21 | 22 | parent::__construct($loader); 23 | } 24 | 25 | public static function make(string $templateDirectory): TwigRenderer 26 | { 27 | return new self($templateDirectory); 28 | } 29 | 30 | public function renderTemplate(string $path, array $variables): string 31 | { 32 | try { 33 | return $this->render($path, $variables); 34 | } catch (Twig_Error_Loader $e) { 35 | throw InvalidConfiguration::templateNotFound($path); 36 | } 37 | } 38 | 39 | public function customExtension(Extension $extension): void 40 | { 41 | $this->addGlobal($extension->name(), $extension); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/ConfigTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('bar', Config::get('nested.item')); 21 | } 22 | 23 | /** @test */ 24 | public function nested_properties_can_be_get_as_array(): void 25 | { 26 | $this->assertTrue(\is_array(Config::get('nested'))); 27 | } 28 | 29 | /** @test */ 30 | public function unknown_property_returns_null(): void 31 | { 32 | $this->assertNull(Config::get('not.known')); 33 | } 34 | 35 | /** @test */ 36 | public function env_function_used_in_config(): void 37 | { 38 | $this->assertEquals('foo', Config::get('with.env')); 39 | } 40 | 41 | /** @test */ 42 | public function env_function(): void 43 | { 44 | $this->assertEquals('foo', env('TEST_KEY')); 45 | } 46 | 47 | /** @test */ 48 | public function config_function(): void 49 | { 50 | $this->assertEquals('test', config('public')); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Stitcher/File.php: -------------------------------------------------------------------------------- 1 | dumpFile(self::path($path), $content); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/StitcherTest.php: -------------------------------------------------------------------------------- 1 | exists($dataDir)) { 33 | $fs->remove($dataDir); 34 | } 35 | 36 | File::base(null); 37 | } 38 | 39 | protected function getProductionPage(string $url): Response 40 | { 41 | $url = ltrim($url, '/'); 42 | 43 | $client = new Client(); 44 | 45 | $host = StitcherTestBootstrap::$productionHost; 46 | 47 | return $client->request('GET', "{$host}/{$url}"); 48 | } 49 | 50 | protected function getDevelopmentPage(string $url): Response 51 | { 52 | $url = ltrim($url, '/'); 53 | 54 | $client = new Client(); 55 | 56 | $host = StitcherTestBootstrap::$developmentHost; 57 | 58 | return $client->request('GET', "{$host}/{$url}"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Item/NameMeta.php: -------------------------------------------------------------------------------- 1 | content; 34 | 35 | if ($this->isTitle() && isset($extra['title']['suffix'])) { 36 | $content = "{$content}{$extra['title']['suffix']}"; 37 | } 38 | 39 | return "name}\" content=\"{$content}\">"; 40 | } 41 | 42 | /** 43 | * NamedMeta constructor. 44 | * 45 | * @param string $name 46 | * @param string $content 47 | */ 48 | public function __construct(string $name, string $content) { 49 | $this->name = $name; 50 | $this->content = htmlentities($content); 51 | } 52 | 53 | private function isTitle(): bool 54 | { 55 | return str_contains($this->name, 'title'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/BasicMetaTest.php: -------------------------------------------------------------------------------- 1 | createPageParser(); 17 | 18 | /** @var \Stitcher\Page\Page $page */ 19 | $page = $pageParser->parse($this->createConfiguration())->first(); 20 | $meta = $page->meta()->render(); 21 | 22 | $this->assertContains('', $meta); 23 | $this->assertContains('', $meta); 24 | $this->assertContains('', $meta); 25 | $this->assertContains('', $meta); 26 | $this->assertContains('', $meta); 27 | $this->assertContains('', $meta); 28 | } 29 | 30 | private function createConfiguration(): array 31 | { 32 | return Yaml::parse(<<content; 34 | 35 | if ($this->isTitle() && isset($extra['title']['suffix'])) { 36 | $content = "{$content}{$extra['title']['suffix']}"; 37 | } 38 | 39 | return "name}\" content=\"{$content}\">"; 40 | } 41 | 42 | /** 43 | * NamedMeta constructor. 44 | * 45 | * @param string $name 46 | * @param string $content 47 | */ 48 | public function __construct(string $name, string $content) { 49 | $this->name = $name; 50 | $this->content = htmlentities($content); 51 | } 52 | 53 | private function isTitle(): bool 54 | { 55 | return str_contains($this->name, 'title'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/PluginTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(TestPlugin::class, $plugin); 21 | } 22 | 23 | /** @test */ 24 | public function a_plugin_can_register_configuration(): void 25 | { 26 | App::init(); 27 | 28 | $this->assertEquals(1, Config::get('test.plugin.item')); 29 | } 30 | 31 | /** @test */ 32 | public function a_plugin_can_register_services(): void 33 | { 34 | App::init(); 35 | 36 | $testPluginService = App::get(TestPluginService::class); 37 | 38 | $this->assertInstanceOf(TestPluginService::class, $testPluginService); 39 | $this->assertEquals(1, $testPluginService->item); 40 | } 41 | 42 | /** @test */ 43 | public function a_plugin_can_be_booted(): void 44 | { 45 | App::init(); 46 | 47 | /** @var TestPlugin $testPlugin */ 48 | $testPlugin = App::get(TestPlugin::class); 49 | 50 | $this->assertInstanceOf(TestPluginService::class, $testPlugin::$service); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Item/ItemPropMeta.php: -------------------------------------------------------------------------------- 1 | content; 34 | 35 | if ($this->isTitle() && isset($extra['title']['suffix'])) { 36 | $content = "{$content}{$extra['title']['suffix']}"; 37 | } 38 | 39 | return "name}\" content=\"{$content}\">"; 40 | } 41 | 42 | /** 43 | * NamedMeta constructor. 44 | * 45 | * @param string $name 46 | * @param string $content 47 | */ 48 | public function __construct(string $name, string $content) { 49 | $this->name = $name; 50 | $this->content = htmlentities($content); 51 | } 52 | 53 | private function isTitle(): bool 54 | { 55 | return str_contains($this->name, 'title') || $this->name === 'name'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Stitcher/Task/ParseTest.php: -------------------------------------------------------------------------------- 1 | createAllTemplates(); 21 | $this->createConfigurationFile(); 22 | 23 | $siteMap = $this->createSiteMap(); 24 | 25 | $command = Parse::make( 26 | File::path('public'), 27 | File::path('site.yaml'), 28 | $this->createPageParser(), 29 | $this->createPageRenderer(), 30 | $siteMap 31 | ); 32 | 33 | $command->addSubTask(new RenderSiteMap(File::path('public'), $siteMap)); 34 | 35 | $command->execute(); 36 | 37 | $this->assertFileExists(File::path('public/index.html')); 38 | $this->assertFileExists(File::path('public/test.html')); 39 | $this->assertFileExists(File::path('public/sitemap.xml')); 40 | } 41 | 42 | private function createConfigurationFile(): void 43 | { 44 | File::write('site.yaml', << '/', 31 | 'template' => 'index.twig', 32 | 'variables' => [ 33 | 'entries' => 'entries.yaml', 34 | ], 35 | 'config' => [ 36 | 'order' => [ 37 | 'variable' => 'entries', 38 | 'field' => 'title', 39 | 'direction' => 'desc', 40 | ], 41 | ], 42 | ]; 43 | 44 | $adapter = OrderAdapter::make($pageConfiguration['config']['order'], $this->createVariableParser()); 45 | 46 | $result = $adapter->transform($pageConfiguration); 47 | 48 | $result = reset($result); 49 | 50 | $entries = $result['variables']['entries']; 51 | 52 | $order = array_keys($entries); 53 | 54 | $this->assertEquals(['c', 'b', 'a'], $order); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Social/GooglePlusMetaTest.php: -------------------------------------------------------------------------------- 1 | meta = new Meta(); 17 | } 18 | 19 | private function createSocialMeta() : SocialMeta { 20 | return new GooglePlusMeta($this->meta); 21 | } 22 | 23 | /** @test */ 24 | public function it_can_render_the_title(): void 25 | { 26 | $social = $this->createSocialMeta(); 27 | 28 | $social->title('hello'); 29 | 30 | $this->assertContains('', $this->meta->render()); 31 | } 32 | 33 | /** @test */ 34 | public function it_can_render_the_description(): void 35 | { 36 | $social = $this->createSocialMeta(); 37 | 38 | $social->description('hello'); 39 | 40 | $this->assertContains('', $this->meta->render()); 41 | } 42 | 43 | /** @test */ 44 | public function it_can_render_the_image(): void 45 | { 46 | $social = $this->createSocialMeta(); 47 | 48 | $social->image('hello'); 49 | 50 | $this->assertContains('', $this->meta->render()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/PageTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Page::class, $page); 15 | } 16 | 17 | /** @test */ 18 | public function it_sets_default_meta_from_variables(): void 19 | { 20 | $page = Page::make('/home', 'index.twig', [ 21 | 'title' => 'title', 22 | 'description' => 'description' 23 | ]); 24 | 25 | $meta = $page->meta()->render(); 26 | $this->assertContains('', $meta); 27 | $this->assertContains('', $meta); 28 | } 29 | 30 | /** @test */ 31 | public function it_sets_default_meta_from_meta_variables(): void 32 | { 33 | $page = Page::make('/home', 'index.twig', [ 34 | 'title' => 'title', 35 | 'description' => 'description', 36 | 'meta' => [ 37 | 'title' => 'title2', 38 | 'description' => 'description2', 39 | ], 40 | ]); 41 | 42 | $meta = $page->meta()->render(); 43 | $this->assertContains('', $meta); 44 | $this->assertContains('', $meta); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/PaginatedMetaTest.php: -------------------------------------------------------------------------------- 1 | createPageParser(); 17 | 18 | $pages = $pageParser->parse($this->createConfiguration()); 19 | 20 | $metaPage1 = $pages->get('test/page-1')->meta()->render(); 21 | $metaPage2 = $pages->get('test/page-2')->meta()->render(); 22 | $metaPage3 = $pages->get('test/page-3')->meta()->render(); 23 | 24 | $this->assertContains('next', $metaPage1); 25 | $this->assertNotContains('prev', $metaPage1); 26 | 27 | $this->assertContains('next', $metaPage2); 28 | $this->assertContains('prev', $metaPage2); 29 | 30 | $this->assertNotContains('next', $metaPage3); 31 | $this->assertContains('prev', $metaPage3); 32 | } 33 | 34 | private function createConfiguration(): array 35 | { 36 | return Yaml::parse(<< '/', 32 | 'template' => 'index.twig', 33 | 'variables' => [ 34 | 'entries' => 'entries.yaml', 35 | ], 36 | 'config' => [ 37 | 'filter' => [ 38 | 'entries' => [ 39 | 'category' => 'blog', 40 | 'name' => 'A', 41 | ], 42 | ], 43 | ], 44 | ]; 45 | 46 | $adapter = FilterAdapter::make($pageConfiguration['config']['filter'], $this->createVariableParser()); 47 | $result = $adapter->transform($pageConfiguration); 48 | $entries = $result['variables']['entries']; 49 | 50 | $this->assertArrayHasKey('a', $entries); 51 | $this->assertArrayNotHasKey('b', $entries); 52 | $this->assertArrayNotHasKey('c', $entries); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/DevelopmentServerTest.php: -------------------------------------------------------------------------------- 1 | getDevelopmentPage('/'); 18 | 19 | $this->assertEquals(200, $response->getStatusCode()); 20 | } 21 | 22 | /** @test */ 23 | public function it_serves_static_html_pages_on_the_development_server(): void 24 | { 25 | $this->parseAll(); 26 | 27 | $body = (string) $this->getDevelopmentPage('/')->getBody(); 28 | $this->assertContains('', $body); 29 | 30 | $body = (string) $this->getDevelopmentPage('/entries/a')->getBody(); 31 | $this->assertContains('', $body); 32 | } 33 | 34 | /** @test */ 35 | public function it_serves_dynamic_pages_on_the_development_server(): void 36 | { 37 | $this->parseAll(); 38 | 39 | $body = (string) $this->getDevelopmentPage('/test/1/abc')->getBody(); 40 | 41 | $this->assertContains('test 1 abc', $body); 42 | } 43 | 44 | /** @test */ 45 | public function it_can_redirect(): void 46 | { 47 | $this->parseAll(); 48 | 49 | $response = $this->getDevelopmentPage('/redirect'); 50 | 51 | $this->assertContains('

A

', (string) $response->getBody()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Social/TwitterMetaTest.php: -------------------------------------------------------------------------------- 1 | meta = new Meta(); 18 | } 19 | 20 | private function createSocialMeta() : SocialMeta { 21 | return new TwitterMeta($this->meta); 22 | } 23 | 24 | /** @test */ 25 | public function it_can_render_the_title(): void 26 | { 27 | $social = $this->createSocialMeta(); 28 | 29 | $social->title('hello'); 30 | 31 | $this->assertContains('', $this->meta->render()); 32 | $this->assertContains('', $this->meta->render()); 33 | } 34 | 35 | /** @test */ 36 | public function it_can_render_the_description(): void 37 | { 38 | $social = $this->createSocialMeta(); 39 | 40 | $social->description('hello'); 41 | 42 | $this->assertContains('', $this->meta->render()); 43 | } 44 | 45 | /** @test */ 46 | public function it_can_render_the_image(): void 47 | { 48 | $social = $this->createSocialMeta(); 49 | 50 | $social->image('hello'); 51 | 52 | $this->assertContains('', $this->meta->render()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/Social/OpenGraphMetaTest.php: -------------------------------------------------------------------------------- 1 | meta = new Meta(); 18 | } 19 | 20 | private function createSocialMeta() : SocialMeta { 21 | return new OpenGraphMeta($this->meta); 22 | } 23 | 24 | /** @test */ 25 | public function it_can_render_the_title(): void 26 | { 27 | $social = $this->createSocialMeta(); 28 | 29 | $social->title('hello'); 30 | 31 | $this->assertContains('', $this->meta->render()); 32 | $this->assertContains('', $this->meta->render()); 33 | } 34 | 35 | /** @test */ 36 | public function it_can_render_the_description(): void 37 | { 38 | $social = $this->createSocialMeta(); 39 | 40 | $social->description('hello'); 41 | 42 | $this->assertContains('', $this->meta->render()); 43 | } 44 | 45 | /** @test */ 46 | public function it_can_render_the_image(): void 47 | { 48 | $social = $this->createSocialMeta(); 49 | 50 | $social->image('hello'); 51 | 52 | $this->assertContains('', $this->meta->render()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Image/ImageFactoryTest.php: -------------------------------------------------------------------------------- 1 | create('resources/images/green_large.jpg'); 22 | 23 | $this->assertNotNull(File::read('public/resources/images/green_large.jpg')); 24 | $this->assertNotNull(File::read('public/resources/images/green_large-500x500.jpg')); 25 | $this->assertNotNull(File::read('public/resources/images/green_large-300x300.jpg')); 26 | } 27 | 28 | /** @test */ 29 | public function it_adds_the_srcset(): void 30 | { 31 | $public = File::path('public'); 32 | 33 | $factory = ImageFactory::make(File::path(), $public, FixedWidthScaler::make([ 34 | 300, 500, 35 | ])); 36 | 37 | $image = $factory->create('resources/images/green_large.jpg'); 38 | $srcset = $image->srcset(); 39 | 40 | $this->assertContains('/resources/images/green_large.jpg 2500w', $srcset); 41 | $this->assertContains('/resources/images/green_large-500x500.jpg 500w', $srcset); 42 | $this->assertContains('/resources/images/green_large-300x300.jpg 300w', $srcset); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/ProductionServerTest.php: -------------------------------------------------------------------------------- 1 | parseAll(); 18 | 19 | $response = $this->getProductionPage('/'); 20 | 21 | $this->assertEquals(200, $response->getStatusCode()); 22 | } 23 | 24 | /** @test */ 25 | public function it_serves_static_html_pages_on_the_production_server(): void 26 | { 27 | $this->parseAll(); 28 | 29 | $body = (string) $this->getProductionPage('/')->getBody(); 30 | $this->assertContains('', $body); 31 | 32 | $body = (string) $this->getProductionPage('/entries/a')->getBody(); 33 | $this->assertContains('', $body); 34 | } 35 | 36 | /** @test */ 37 | public function it_serves_dynamic_pages_on_the_production_server(): void 38 | { 39 | $this->parseAll(); 40 | 41 | $body = (string) $this->getProductionPage('/test/1/abc')->getBody(); 42 | 43 | $this->assertContains('test 1 abc', $body); 44 | } 45 | 46 | /** @test */ 47 | public function it_can_redirect(): void 48 | { 49 | $this->parseAll(); 50 | 51 | $response = $this->getDevelopmentPage('/redirect'); 52 | 53 | $this->assertContains('

A

', (string) $response->getBody()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/CollectionMetaTest.php: -------------------------------------------------------------------------------- 1 | createPageParser(); 17 | 18 | $pages = $pageParser->parse($this->createConfiguration()); 19 | 20 | $metaPage1 = $pages['/a']->meta()->render(); 21 | $metaPage2 = $pages['/b']->meta()->render(); 22 | 23 | $this->assertContains('', $metaPage1); 24 | $this->assertContains('', $metaPage1); 25 | 26 | $this->assertContains('', $metaPage2); 27 | $this->assertContains('', $metaPage2); 28 | } 29 | 30 | private function createConfiguration(): array 31 | { 32 | return Yaml::parse(<<addRenderer(Link::class, new ExternalLinkRenderer()) 37 | ->addRenderer(Image::class, new ImageRenderer($imageFactory)); 38 | 39 | foreach (self::$extensions as $closure) { 40 | $environment = $closure($environment); 41 | } 42 | 43 | $this->converter = new MarkdownConverter($environment); 44 | } 45 | 46 | public function parse(?string $markdown): ?string 47 | { 48 | if (! $markdown) { 49 | return null; 50 | } 51 | 52 | return $this->converter->convertToHtml($markdown); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/PageFactoryTest.php: -------------------------------------------------------------------------------- 1 | create([ 22 | 'id' => '/', 23 | 'template' => 'index.twig', 24 | ]); 25 | 26 | $this->assertInstanceOf(Page::class, $page); 27 | } 28 | 29 | /** @test */ 30 | public function it_throws_an_exception_when_id_is_missing(): void 31 | { 32 | $this->expectException(InvalidConfiguration::class); 33 | 34 | $factory = new PageFactory( 35 | VariableParser::make( 36 | VariableFactory::make() 37 | ) 38 | ); 39 | 40 | $factory->create([ 41 | 'template' => 'index.twig', 42 | ]); 43 | } 44 | 45 | /** @test */ 46 | public function it_throws_an_exception_when_template_is_missing(): void 47 | { 48 | $this->expectException(InvalidConfiguration::class); 49 | 50 | $factory = new PageFactory( 51 | VariableParser::make( 52 | VariableFactory::make() 53 | ) 54 | ); 55 | 56 | $factory->create([ 57 | 'id' => '/', 58 | ]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "pageon/stitcher-core", 3 | "description" : "Static website generator in PHP", 4 | "prefer-stable" : true, 5 | "require" : { 6 | "php": "^8.0", 7 | "symfony/yaml" : "^4.0", 8 | "erusev/parsedown" : "^1.6", 9 | "symfony/filesystem" : "^4.0", 10 | "twig/twig" : "^2.8", 11 | "intervention/image" : "^2.4", 12 | "vlucas/phpdotenv" : "^5.3", 13 | "symfony/finder": "^4.0", 14 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0", 15 | "symfony/routing": "^4.0", 16 | "symfony/dependency-injection": "^4.0", 17 | "symfony/config": "^4.0", 18 | "leafo/scssphp": "^0.7.1", 19 | "nikic/fast-route": "^1.2", 20 | "spatie/image-optimizer": "^1.1", 21 | "league/commonmark": "^2.0", 22 | "phpoption/phpoption": "^1.7", 23 | "matthiasmullie/minify": "^1.3" 24 | }, 25 | "require-dev" : { 26 | "larapack/dd" : "^1.0", 27 | "phpunit/phpunit" : "^9.0", 28 | "symfony/process": "^4.0", 29 | "guzzlehttp/guzzle": "^6.3" 30 | }, 31 | "autoload" : { 32 | "psr-4" : { 33 | "Stitcher\\" : "src/Stitcher", 34 | "Pageon\\" : "src/Pageon" 35 | } 36 | }, 37 | "autoload-dev" : { 38 | "psr-4" : { 39 | "Stitcher\\Test\\" : "tests", 40 | "Stitcher\\Test\\Plugin\\" : "tests/test_project/src/Plugin", 41 | "Stitcher\\Test\\Controller\\" : "tests/test_project/src/Controller", 42 | "Pageon\\Test\\" : "tests" 43 | }, 44 | "files": [ 45 | "src/Pageon/helpers.php" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Stitcher/Variable/ImageVariableTest.php: -------------------------------------------------------------------------------- 1 | createImageFactory() 19 | )->parse(); 20 | 21 | $parsed = $variable->getParsed(); 22 | $this->assertArrayHasKey('src', $parsed, '`src` not found in parsed image.'); 23 | $this->assertArrayHasKey('srcset', $parsed, '`srcset not found in parsed image.`'); 24 | $this->assertEquals('/resources/images/green.jpg', $parsed['src'], '`src` does not match expected value in parsed image.'); 25 | } 26 | 27 | /** @test */ 28 | public function it_can_be_parsed_with_alt(): void 29 | { 30 | $variable = ImageVariable::make( 31 | [ 32 | 'src' => '/resources/images/green.jpg', 33 | 'alt' => 'test', 34 | ], 35 | $this->createImageFactory() 36 | )->parse(); 37 | 38 | $parsed = $variable->getParsed(); 39 | $this->assertArrayHasKey('alt', $parsed, '`alt not found in parsed image.`'); 40 | $this->assertEquals('test', $parsed['alt'], '`alt` does not match expected value in parsed image.'); 41 | $this->assertEquals('/resources/images/green.jpg', $parsed['src'], '`src` does not match expected value in parsed image.'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Stitcher/Application/DevelopmentServer.php: -------------------------------------------------------------------------------- 1 | rootDirectory = $rootDirectory; 26 | $this->path = $path; 27 | $this->partialParse = $partialParse; 28 | } 29 | 30 | public static function make( 31 | string $rootDirectory, 32 | PartialParse $partialParse, 33 | string $path = null 34 | ): DevelopmentServer 35 | { 36 | return new self($rootDirectory, $partialParse, $path); 37 | } 38 | 39 | protected function handleStaticRoute(): ?Response 40 | { 41 | $path = $this->path ?? $this->getCurrentPath(); 42 | 43 | $this->partialParse->setFilter($path); 44 | 45 | try { 46 | $this->partialParse->execute(); 47 | 48 | $filename = ltrim($path === '/' ? 'index.html' : "{$path}.html", '/'); 49 | 50 | $body = @file_get_contents("{$this->rootDirectory}/{$filename}"); 51 | 52 | return $body ? new Response(200, [], $body) : null; 53 | } catch (ResourceNotFoundException $e) { 54 | return null; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Stitcher/Application/DevelopmentServerTest.php: -------------------------------------------------------------------------------- 1 | createAllTemplates(); 26 | $this->createSiteConfiguration($configurationFile); 27 | $this->createDataFile(); 28 | $this->createImageFiles(); 29 | 30 | $this->command = PartialParse::make( 31 | File::path('public'), 32 | $configurationFile, 33 | $this->createPageParser(), 34 | $this->createPageRenderer(), 35 | $this->createSiteMap() 36 | ); 37 | } 38 | 39 | /** @test */ 40 | public function it_serves_static_html(): void 41 | { 42 | $server = DevelopmentServer::make(File::path('public'), $this->command, '/entries'); 43 | 44 | $html = $server->run(); 45 | 46 | $this->assertContains('', $html); 47 | } 48 | 49 | /** @test */ 50 | public function it_serves_static_html_from_index(): void 51 | { 52 | $server = DevelopmentServer::make(File::path('public'), $this->command, '/'); 53 | 54 | $html = $server->run(); 55 | 56 | $this->assertContains('', $html); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Stitcher/Variable/VariableFactoryTest.php: -------------------------------------------------------------------------------- 1 | setYamlParser(new Yaml()) 23 | ->setMarkdownParser($this->createMarkdownParser()) 24 | ->setImageParser($this->createImageFactory()) 25 | ->setVariableParser($this->createVariableParser()); 26 | 27 | $this->assertInstanceOf(ImageVariable::class, $factory->create('image.jpg')); 28 | $this->assertInstanceOf(JsonVariable::class, $factory->create('test.json')); 29 | $this->assertInstanceOf(YamlVariable::class, $factory->create('test.yaml')); 30 | $this->assertInstanceOf(YamlVariable::class, $factory->create('test.yml')); 31 | $this->assertInstanceOf(MarkdownVariable::class, $factory->create('test.md')); 32 | $this->assertInstanceOf(ImageVariable::class, $factory->create('image.jpeg')); 33 | $this->assertInstanceOf(ImageVariable::class, $factory->create('image.png')); 34 | $this->assertInstanceOf(ImageVariable::class, $factory->create('image.gif')); 35 | $this->assertInstanceOf(ImageVariable::class, $factory->create([ 36 | 'src' => 'image.jpeg', 37 | ])); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/Adapter/CollectionAdapterTest.php: -------------------------------------------------------------------------------- 1 | '/{id}', 26 | 'template' => 'index.twig', 27 | 'variables' => [ 28 | 'entry' => 'entries.yaml', 29 | ], 30 | 'config' => [ 31 | 'collection' => [ 32 | 'variable' => 'entry', 33 | 'parameter' => 'id', 34 | ], 35 | ], 36 | ]; 37 | 38 | $adapter = CollectionAdapter::make($pageConfiguration['config']['collection'], $this->createVariableParser()); 39 | $result = $adapter->transform($pageConfiguration); 40 | 41 | $this->assertTrue(\is_array($result)); 42 | $this->assertArrayHasKey('/a', $result); 43 | $this->assertEquals('A', $result['/a']['variables']['entry']['name']); 44 | $this->assertArrayHasKey('/b', $result); 45 | $this->assertEquals('B', $result['/b']['variables']['entry']['name']); 46 | 47 | $this->assertNull($result['/a']['variables']['_browse']['prev'] ?? null); 48 | $this->assertNotNull($result['/a']['variables']['_browse']['next'] ?? null); 49 | 50 | $this->assertNotNull($result['/b']['variables']['_browse']['prev'] ?? null); 51 | $this->assertNull($result['/b']['variables']['_browse']['next'] ?? null); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Stitcher/Variable/VariableParserTest.php: -------------------------------------------------------------------------------- 1 | createVariableParser(); 24 | $parsed = $variableParser->parse($path); 25 | 26 | $this->assertTrue(\is_array($parsed)); 27 | $this->assertTrue(isset($parsed['entry']['title'])); 28 | } 29 | 30 | /** @test */ 31 | public function it_can_be_parsed_recursively(): void 32 | { 33 | $path = File::path('YamlVariableTest_test_recursive_parent.yaml'); 34 | $this->createRecursiveFiles($path); 35 | 36 | $variableParser = $this->createVariableParser(); 37 | $parsed = $variableParser->parse($path); 38 | 39 | $this->assertTrue(isset($parsed['entry']['child']['title'])); 40 | } 41 | 42 | private function createRecursiveFiles(string $path): void 43 | { 44 | $parentPath = File::path($path); 45 | File::write($parentPath, <<createExtension(); 16 | 17 | $css->parseSource('/resources/css/normal.css'); 18 | 19 | $this->assertNotNull(File::read('public/resources/css/normal.css')); 20 | $this->assertContains('body', File::read('public/resources/css/normal.css')); 21 | } 22 | 23 | /** @test */ 24 | public function it_moves_and_parses_a_scss_file(): void 25 | { 26 | $css = $this->createExtension(); 27 | 28 | $css->parseSource('/resources/css/scss_file.scss'); 29 | 30 | $this->assertNotNull(File::read('public/resources/css/scss_file.css')); 31 | $this->assertContains('body h1', File::read('public/resources/css/scss_file.css')); 32 | } 33 | 34 | /** @test */ 35 | public function test_inline(): void 36 | { 37 | $css = $this->createExtension(); 38 | 39 | $style = $css->inline('/resources/css/scss_file.scss'); 40 | 41 | $this->assertContains('assertContains('body h1', $style); 43 | } 44 | 45 | /** @test */ 46 | public function test_link(): void 47 | { 48 | $css = $this->createExtension(); 49 | 50 | $style = $css->link('/resources/css/normal.css'); 51 | 52 | $this->assertContains('assertContains('rel="stylesheet"', $style); 54 | $this->assertContains('href="/resources/css/normal.css"', $style); 55 | } 56 | 57 | private function createExtension(): Css 58 | { 59 | App::init(); 60 | 61 | return App::get('cssExtension'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Stitcher/Renderer/RendererFactory.php: -------------------------------------------------------------------------------- 1 | templateDirectory = $templateDirectory; 18 | $this->rendererConfiguration = $rendererConfiguration; 19 | 20 | $this->setTwigRule(); 21 | } 22 | 23 | public static function make( 24 | string $templateDirectory, 25 | ?string $renderer = 'twig' 26 | ): RendererFactory { 27 | return new self($templateDirectory, $renderer); 28 | } 29 | 30 | public function addExtension(Extension $extension): void 31 | { 32 | $this->extensions[$extension->name()] = $extension; 33 | } 34 | 35 | public function create(): ?Renderer 36 | { 37 | foreach ($this->getRules() as $rule) { 38 | $templateRenderer = $rule($this->rendererConfiguration); 39 | 40 | if ($templateRenderer) { 41 | $this->loadExtensions($templateRenderer); 42 | 43 | return $templateRenderer; 44 | } 45 | } 46 | 47 | return null; 48 | } 49 | 50 | protected function loadExtensions(Renderer $renderer): void 51 | { 52 | foreach ($this->extensions as $extension) { 53 | $renderer->customExtension($extension); 54 | } 55 | } 56 | 57 | private function setTwigRule(): void 58 | { 59 | $this->setRule(TwigRenderer::class, function ($value) { 60 | if ($value === 'twig') { 61 | return TwigRenderer::make($this->templateDirectory); 62 | } 63 | 64 | return null; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Pageon/Lib/Markdown/ImageRenderer.php: -------------------------------------------------------------------------------- 1 | imageFactory = $imageFactory; 24 | } 25 | 26 | public function render(Node $node, ChildNodeRendererInterface $childRenderer) 27 | { 28 | if (! $node instanceof Image) { 29 | throw new InvalidArgumentException('Inline must be instance of ' . Image::class); 30 | } 31 | 32 | $attributes = []; 33 | 34 | $src = $node->getUrl(); 35 | 36 | try { 37 | $responsiveImage = $this->imageFactory->create($src); 38 | } catch (FileNotFoundException $e) { 39 | throw InvalidConfiguration::fileNotFound($src); 40 | } 41 | 42 | $alt = $node->firstChild(); 43 | 44 | $attributes['src'] = $src; 45 | $attributes['srcset'] = $responsiveImage->srcset() ?? ''; 46 | $attributes['sizes'] = $responsiveImage->sizes() ?? ''; 47 | $attributes['alt'] = $alt instanceof Text 48 | ? $alt->getLiteral() 49 | : ''; 50 | 51 | return new HtmlElement( 52 | 'img', 53 | $attributes, 54 | $childRenderer->renderNodes($node->children()) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Stitcher/Renderer/Extension/JsTest.php: -------------------------------------------------------------------------------- 1 | createExtension(); 14 | 15 | $js->parseSource('/resources/js/main.js'); 16 | 17 | $this->assertNotNull(File::read('public/resources/js/main.js')); 18 | $this->assertContains('console.log', File::read('public/resources/js/main.js')); 19 | } 20 | 21 | /** @test */ 22 | public function test_inline(): void 23 | { 24 | $js = $this->createExtension(); 25 | 26 | $script = $js->inline('/resources/js/main.js'); 27 | 28 | $this->assertContains('assertContains('console.log', $script); 30 | } 31 | 32 | /** @test */ 33 | public function test_link(): void 34 | { 35 | $js = $this->createExtension(); 36 | 37 | $script = $js->link('/resources/js/main.js'); 38 | 39 | $this->assertContains('assertContains('', $script); 41 | $this->assertContains('src="/resources/js/main.js"', $script); 42 | } 43 | 44 | /** @test */ 45 | public function test_defer(): void 46 | { 47 | $js = $this->createExtension(); 48 | 49 | $script = $js->defer()->link('/resources/js/main.js'); 50 | 51 | $this->assertContains('defer', $script); 52 | } 53 | 54 | /** @test */ 55 | public function test_async(): void 56 | { 57 | $js = $this->createExtension(); 58 | 59 | $script = $js->async()->link('/resources/js/main.js'); 60 | 61 | $this->assertContains('async', $script); 62 | } 63 | 64 | private function createExtension(): Js 65 | { 66 | return new Js( 67 | File::path('public') 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Stitcher/Task/PartialParseTest.php: -------------------------------------------------------------------------------- 1 | createAllTemplates(); 25 | $this->createSiteConfiguration($configurationFile); 26 | $this->createDataFile(); 27 | $this->createImageFiles(); 28 | 29 | $this->command = PartialParse::make( 30 | File::path('public'), 31 | $configurationFile, 32 | $this->createPageParser(), 33 | $this->createPageRenderer(), 34 | $this->createSiteMap() 35 | ); 36 | } 37 | 38 | /** @test */ 39 | public function it_parses_only_one_page(): void 40 | { 41 | $this->command->setFilter('/entries'); 42 | 43 | $this->command->execute(); 44 | 45 | $this->assertFileExists(File::path('public/entries.html')); 46 | $this->assertFileNotExists(File::path('public/index.html')); 47 | } 48 | 49 | /** @test */ 50 | public function it_parses_a_collection(): void 51 | { 52 | $this->command->setFilter('/entries/a'); 53 | 54 | $this->command->execute(); 55 | 56 | $this->assertFileExists(File::path('public/entries/a.html')); 57 | } 58 | 59 | /** @test */ 60 | public function it_parses_a_paginated_page(): void 61 | { 62 | $this->command->setFilter('/entries/page-1'); 63 | 64 | $this->command->execute(); 65 | 66 | $this->assertFileExists(File::path('public/entries/page-1.html')); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Pageon/Html/Meta/MetaTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($meta); 18 | } 19 | 20 | public function test_social_meta_title(): void 21 | { 22 | $meta = Meta::create(); 23 | $meta->title('test'); 24 | $html = $meta->render(); 25 | 26 | $this->assertContains('assertContains('assertContains('image('test'); 46 | $html = $meta->render(); 47 | 48 | $this->assertContains('assertContains('publicDirectory}/{$staticFile}"); 42 | 43 | if ($this->cacheStaticFiles && $this->fs->exists($publicPath)) { 44 | continue; 45 | } 46 | 47 | $sourcePath = File::path($staticFile); 48 | 49 | try { 50 | $this->copyStaticFile($sourcePath, $publicPath); 51 | } catch (FileNotFoundException $exception) { 52 | throw FileNotFound::staticFile($sourcePath); 53 | } 54 | } 55 | } 56 | 57 | private function copyStaticFile( 58 | string $sourcePath, 59 | string $publicPath 60 | ): void { 61 | if (is_dir($sourcePath)) { 62 | $this->fs->mirror($sourcePath, $publicPath); 63 | 64 | return; 65 | } 66 | 67 | $this->fs->copy($sourcePath, $publicPath); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/YamlVariable.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 23 | $this->variableParser = $variableParser; 24 | } 25 | 26 | public static function make( 27 | string $value, 28 | Yaml $parser, 29 | VariableParser $variableParser 30 | ): YamlVariable { 31 | return new self($value, $parser, $variableParser); 32 | } 33 | 34 | public function parse(): AbstractVariable 35 | { 36 | $this->parsed = $this->parser->parse(File::read($this->unparsed)); 37 | 38 | foreach ($this->parsed as $id => $parsedItem) { 39 | if (! \is_array($parsedItem) || isset($parsedItem['id'])) { 40 | continue; 41 | } 42 | 43 | $parsedItem['id'] = $id; 44 | 45 | $this->parsed[$id] = $parsedItem; 46 | } 47 | 48 | $this->parsed = $this->parseRecursive($this->parsed); 49 | 50 | return $this; 51 | } 52 | 53 | private function parseRecursive($unparsedValue) 54 | { 55 | $unparsedValue = $this->variableParser->getVariable($unparsedValue); 56 | 57 | if ($unparsedValue instanceof DefaultVariable) { 58 | $parsedValue = $unparsedValue->getParsed(); 59 | 60 | if (\is_array($parsedValue)) { 61 | foreach ($parsedValue as $key => &$property) { 62 | $property = $this->parseRecursive($property); 63 | } 64 | } 65 | } else { 66 | $parsedValue = $unparsedValue->getParsed(); 67 | } 68 | 69 | return $parsedValue; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Stitcher/Task/PartialParse.php: -------------------------------------------------------------------------------- 1 | filter = $filter; 21 | 22 | return $this; 23 | } 24 | 25 | public function execute(): void 26 | { 27 | $parsedConfiguration = $this->getParsedConfiguration(); 28 | 29 | $routeCollection = $this->createRouteCollection($parsedConfiguration); 30 | 31 | $matcher = new UrlMatcher($routeCollection, new RequestContext()); 32 | 33 | $matchingRoute = $matcher->match($this->filter); 34 | 35 | CollectionAdapter::setFilterId($matchingRoute['id'] ?? null); 36 | 37 | $filteredConfiguration = array_filter( 38 | $parsedConfiguration, 39 | function ($key) use ($matchingRoute) { 40 | return $key === $matchingRoute['_route']; 41 | }, ARRAY_FILTER_USE_KEY 42 | ); 43 | 44 | $pages = $this->parsePageConfiguration($filteredConfiguration); 45 | 46 | $this->renderPages($pages); 47 | 48 | $this->executeSubTasks(); 49 | } 50 | 51 | private function createRouteCollection(array $configuration): RouteCollection 52 | { 53 | $routeCollection = new RouteCollection(); 54 | 55 | foreach ($configuration as $id => $pageConfiguration) { 56 | $routeCollection->add($id, new Route($id)); 57 | } 58 | 59 | return $routeCollection; 60 | } 61 | 62 | private function getParsedConfiguration(): array 63 | { 64 | $configurationFile = File::read($this->configurationFile); 65 | 66 | if (! $configurationFile) { 67 | throw InvalidConfiguration::siteConfigurationFileNotFound(); 68 | } 69 | 70 | return (array) Yaml::parse($configurationFile); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Stitcher/AppTest.php: -------------------------------------------------------------------------------- 1 | assertServiceRegistered($class); 49 | } 50 | 51 | $this->assertInstanceOf(Renderer::class, App::get('renderer')); 52 | } 53 | 54 | /** @test */ 55 | public function it_can_get_the_development_server(): void 56 | { 57 | App::init(); 58 | 59 | $this->assertInstanceOf(DevelopmentServer::class, App::developmentServer()); 60 | } 61 | 62 | /** @test */ 63 | public function it_can_get_the_production_server(): void 64 | { 65 | App::init(); 66 | 67 | $this->assertInstanceOf(ProductionServer::class, App::productionServer()); 68 | } 69 | 70 | private function assertServiceRegistered(string $class): void 71 | { 72 | $this->assertInstanceOf($class, App::get($class)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Stitcher/Renderer/Extension/Css.php: -------------------------------------------------------------------------------- 1 | publicDirectory = $publicDirectory; 27 | $this->sass = $sass; 28 | } 29 | 30 | public function setMinify(bool $minify): self 31 | { 32 | $this->minify = $minify; 33 | 34 | return $this; 35 | } 36 | 37 | public function name(): string 38 | { 39 | return 'css'; 40 | } 41 | 42 | public function link(string $src): string 43 | { 44 | $source = $this->parseSource($src); 45 | 46 | return "url()}\" />"; 47 | } 48 | 49 | public function inline(string $src): string 50 | { 51 | $source = $this->parseSource($src); 52 | 53 | return ''; 54 | } 55 | 56 | public function parseSource(string $src): Source 57 | { 58 | $src = ltrim($src, '/'); 59 | 60 | ['dirname' => $dirname, 'filename' => $filename, 'extension' => $extension] = pathinfo($src); 61 | 62 | $content = File::read($src); 63 | 64 | if (\in_array($extension, ['scss', 'sass'])) { 65 | $content = $this->sass->compile($content); 66 | $extension = 'css'; 67 | } 68 | 69 | if ($this->minify) { 70 | // $content = $this->minifier->run($content); 71 | } 72 | 73 | $path = "{$dirname}/{$filename}.{$extension}"; 74 | 75 | $this->saveFile($path, $content); 76 | 77 | return new Source("/{$path}", $content); 78 | } 79 | 80 | protected function saveFile(string $path, string $content): void 81 | { 82 | $fs = new Filesystem(); 83 | 84 | $fs->dumpFile( 85 | File::path("{$this->publicDirectory}/{$path}"), 86 | $content 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Stitcher/Page/Adapter/FilterAdapter.php: -------------------------------------------------------------------------------- 1 | isValidConfiguration($adapterConfiguration)) { 23 | throw InvalidConfiguration::invalidAdapterConfiguration('filter', '`field`: `filter`'); 24 | } 25 | 26 | $this->filters = $adapterConfiguration; 27 | $this->variableParser = $variableParser; 28 | } 29 | 30 | public static function make( 31 | array $adapterConfiguration, 32 | VariableParser $variableParser 33 | ): FilterAdapter { 34 | return new self($adapterConfiguration, $variableParser); 35 | } 36 | 37 | public function transform(array $pageConfiguration): array 38 | { 39 | foreach ($this->filters as $variableName => $filterConfiguration) { 40 | $variable = $pageConfiguration['variables'][$variableName] ?? null; 41 | 42 | $entries = $this->variableParser->parse($variable) ?? []; 43 | 44 | $filteredEntries = $this->filterEntries($filterConfiguration, $entries); 45 | 46 | $pageConfiguration['variables'][$variableName] = $filteredEntries; 47 | } 48 | 49 | unset($pageConfiguration['config']['filter']); 50 | 51 | return [$pageConfiguration]; 52 | } 53 | 54 | public function isValidConfiguration($subject): bool 55 | { 56 | return \is_array($subject); 57 | } 58 | 59 | private function filterEntries($filterConfiguration, $entries): array 60 | { 61 | foreach ($filterConfiguration as $filterField => $filterValue) { 62 | foreach ($entries as $entryId => $entry) { 63 | $value = $entry[$filterField] ?? null; 64 | 65 | if ($value !== $filterValue) { 66 | unset($entries[$entryId]); 67 | } 68 | } 69 | } 70 | 71 | return $entries; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/StitcherTestBootstrap.php: -------------------------------------------------------------------------------- 1 | stopServer(); 26 | }); 27 | 28 | $this->startServer(); 29 | } 30 | 31 | protected function startServer(): void 32 | { 33 | $this->startProductionServer(); 34 | $this->startDevelopmentServer(); 35 | } 36 | 37 | protected function stopServer(): void 38 | { 39 | $this->stopProductionServer(); 40 | $this->stopDevelopmentServer(); 41 | } 42 | 43 | protected function startProductionServer(): void 44 | { 45 | if (self::$productionServerProcess) { 46 | self::$productionServerProcess->stop(); 47 | } 48 | 49 | $host = self::$productionHost; 50 | $documentRoot = File::path('public'); 51 | $router = "{$documentRoot}/index.php"; 52 | 53 | self::$productionServerProcess = new Process("php -S {$host} {$router} >/dev/null 2>&1 & echo $!"); 54 | self::$productionServerProcess->start(); 55 | } 56 | 57 | protected function startDevelopmentServer(): void 58 | { 59 | if (self::$developmentServerProcess) { 60 | self::$developmentServerProcess->stop(); 61 | } 62 | 63 | $host = self::$developmentHost; 64 | $documentRoot = File::path('public'); 65 | $router = "{$documentRoot}/index.php"; 66 | 67 | self::$developmentServerProcess = new Process("ENV=\"development\" php -S {$host} {$router} >/dev/null 2>&1 & echo $!"); 68 | self::$developmentServerProcess->start(); 69 | } 70 | 71 | protected function stopProductionServer(): void 72 | { 73 | if (! self::$productionServerProcess) { 74 | return; 75 | } 76 | 77 | self::$productionServerProcess->stop(); 78 | self::$productionServerProcess = null; 79 | } 80 | 81 | protected function stopDevelopmentServer(): void 82 | { 83 | if (! self::$developmentServerProcess) { 84 | return; 85 | } 86 | 87 | self::$developmentServerProcess->stop(); 88 | self::$developmentServerProcess = null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stitcher [![Build Status](https://scrutinizer-ci.com/g/pageon/stitcher-core/badges/build.png?b=develop)](https://scrutinizer-ci.com/g/pageon/stitcher-core/build-status/develop) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/pageon/stitcher-core/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/pageon/stitcher-core/?branch=develop) [![Code Coverage](https://scrutinizer-ci.com/g/pageon/stitcher-core/badges/coverage.png?b=develop)](https://scrutinizer-ci.com/g/pageon/stitcher-core/?branch=develop) 2 | 3 | 4 | High performance, static websites for PHP developers. 5 | 6 | ```sh 7 | composer create-project pageon/stitcher 8 | ``` 9 | 10 | ### Why Stitcher? 11 | 12 | Stitcher differs from many other static site generator in two areas. First of all: **performance is key**. Stitcher is built from its core for high performance websites. All tools available to you put performance on the first place. Secondly, it doesn't try to add extra syntax to existing formats. Stitcher provides a robust set of tools **for developers** to build with, and not a lot of hacks so everything fits in one file. 13 | 14 | Also important to note, included with Stitcher: 15 | 16 | - Automatic image optimization, as easy as `image.srcset` 17 | - HTTP/2 server push support 18 | - Markdown, YAML and JSON 19 | - Twig and Smarty support 20 | - Data set overviews and details; pagination, sorting and filtering 21 | - Built-in SASS support 22 | - JavaScript and CSS minification 23 | - Built-in SEO and meta tag optimizations 24 | 25 | A quick look at Stitcher: 26 | 27 | ```yaml 28 | # site.yml 29 | 30 | /blog: 31 | template: blog 32 | variables: 33 | posts: data/blog.yml 34 | 35 | /blog/{id}: 36 | template: blog.post 37 | variables: 38 | post: data/blog.yml 39 | adapters: 40 | collection: 41 | variable: post 42 | field: id 43 | ``` 44 | 45 | ```yaml 46 | # data/blog.yml 47 | 48 | hello_world: 49 | date: 2017-03-10 50 | highlight: false 51 | title: Hello world 52 | content: blog/hello.md 53 | image: hello_world.jpg 54 | 55 | foo_bar: 56 | date: 2017-03-14 57 | highlight: true 58 | title: Foo Bar 59 | content: blog/far_bar.md 60 | image: foo_bar.jpg 61 | ``` 62 | 63 | ```html 64 | 65 | 66 | {% extends 'index.html' %} 67 | 68 | {% block content %} 69 |
70 |

{{ blog.title }}

71 | 72 | {{ blog.image.alt }} 75 | 76 | {{ blog.content }} 77 |
78 | {% endblock %} 79 | ``` 80 | 81 | You can read more about it on [the Stitcher website](https://www.stitcher.io). 82 | 83 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/PageParserTest.php: -------------------------------------------------------------------------------- 1 | createVariableParser(); 17 | $parser = PageParser::make($this->createPageFactory($variableParser), $this->createAdapterFactory($variableParser)); 18 | 19 | $page = $parser->parse([ 20 | 'id' => '/', 21 | 'template' => 'index.twig', 22 | ])->first(); 23 | 24 | $this->assertInstanceOf(Page::class, $page); 25 | } 26 | 27 | /** @test */ 28 | public function it_can_parse_variables(): void 29 | { 30 | $markdownPath = File::path('test.md'); 31 | File::write($markdownPath, <<createVariableParser(); 37 | $parser = PageParser::make($this->createPageFactory($variableParser), $this->createAdapterFactory($variableParser)); 38 | $page = $parser->parse([ 39 | 'id' => '/', 40 | 'template' => 'index.twig', 41 | 'variables' => [ 42 | 'title' => 'Test', 43 | 'body' => 'test.md', 44 | ], 45 | ])->first(); 46 | 47 | $this->assertEquals('Test', $page->variable('title')); 48 | $this->assertEquals('

Hello world

', trim($page->variable('body'))); 49 | } 50 | 51 | /** @test */ 52 | public function it_can_parse_a_collection_of_pages(): void 53 | { 54 | File::write('entries.yaml', <<createVariableParser(); 63 | $parser = PageParser::make( 64 | $this->createPageFactory($variableParser), 65 | $this->createAdapterFactory($variableParser) 66 | ); 67 | 68 | $result = $parser->parse([ 69 | 'id' => '/{id}', 70 | 'template' => 'index.twig', 71 | 'variables' => [ 72 | 'entry' => 'entries.yaml', 73 | ], 74 | 'config' => [ 75 | 'collection' => [ 76 | 'variable' => 'entry', 77 | 'parameter' => 'id', 78 | ], 79 | ], 80 | ]); 81 | 82 | $this->assertArrayHasKey('/a', $result); 83 | $this->assertArrayHasKey('/b', $result); 84 | 85 | $pageA = $result['/a']; 86 | $this->assertEquals('A', $pageA->variable('entry')['name']); 87 | 88 | $pageB = $result['/b']; 89 | $this->assertEquals('B', $pageB->variable('entry')['name']); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Stitcher/Renderer/Extension/Js.php: -------------------------------------------------------------------------------- 1 | publicDirectory = $publicDirectory; 30 | } 31 | 32 | public function setMinify(bool $minify): self 33 | { 34 | $this->minify = $minify; 35 | 36 | return $this; 37 | } 38 | 39 | public function name(): string 40 | { 41 | return 'js'; 42 | } 43 | 44 | public function link(string $src): string 45 | { 46 | $source = $this->parseSource($src); 47 | 48 | $script = "'; 59 | 60 | return $script; 61 | } 62 | 63 | public function inline(string $src): string 64 | { 65 | $source = $this->parseSource($src); 66 | 67 | return ''; 68 | } 69 | 70 | public function defer(): Js 71 | { 72 | $this->defer = true; 73 | 74 | return $this; 75 | } 76 | 77 | public function async(): Js 78 | { 79 | $this->async = true; 80 | 81 | return $this; 82 | } 83 | 84 | public function parseSource(string $src): Source 85 | { 86 | $src = ltrim($src, '/'); 87 | 88 | ['dirname' => $dirname, 'filename' => $filename, 'extension' => $extension] = pathinfo($src); 89 | 90 | $content = File::read($src); 91 | 92 | if (!$content) { 93 | throw InvalidConfiguration::fileNotFound(File::path($src)); 94 | } 95 | 96 | if ($this->minify) { 97 | // $content = JSMin::minify($content); 98 | } 99 | 100 | $path = "{$dirname}/{$filename}.{$extension}"; 101 | 102 | $this->saveFile($path, $content); 103 | 104 | return new Source("/{$path}", $content); 105 | } 106 | 107 | protected function saveFile(string $path, string $content): void 108 | { 109 | $fs = new Filesystem(); 110 | 111 | $fs->dumpFile( 112 | File::path("{$this->publicDirectory}/{$path}"), 113 | $content 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Stitcher/Page/Adapter/AdapterFactory.php: -------------------------------------------------------------------------------- 1 | setCollectionRule(); 17 | $this->setFilterRule(); 18 | $this->setPaginationRule(); 19 | $this->setOrderRule(); 20 | 21 | $this->variableParser = $variableParser; 22 | } 23 | 24 | public static function make(VariableParser $variableParser): AdapterFactory 25 | { 26 | return new self($variableParser); 27 | } 28 | 29 | public function create($adapterType, $adapterConfiguration): ?Adapter 30 | { 31 | foreach ($this->getRules() as $rule) { 32 | $adapter = $rule($adapterType, (array) $adapterConfiguration); 33 | 34 | if ($adapter) { 35 | return $adapter; 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | 42 | private function setCollectionRule(): void 43 | { 44 | $this->setRule( 45 | CollectionAdapter::class, 46 | function (string $adapterType, array $adapterConfiguration) { 47 | if ($adapterType !== 'collection') { 48 | return null; 49 | } 50 | 51 | return CollectionAdapter::make($adapterConfiguration, $this->variableParser); 52 | } 53 | ); 54 | } 55 | 56 | private function setFilterRule(): void 57 | { 58 | $this->setRule( 59 | FilterAdapter::class, 60 | function (string $adapterType, array $adapterConfiguration) { 61 | if ($adapterType !== 'filter') { 62 | return null; 63 | } 64 | 65 | return FilterAdapter::make($adapterConfiguration, $this->variableParser); 66 | } 67 | ); 68 | } 69 | 70 | private function setPaginationRule(): void 71 | { 72 | $this->setRule( 73 | PaginationAdapter::class, 74 | function (string $adapterType, array $adapterConfiguration) { 75 | if ($adapterType !== 'pagination') { 76 | return null; 77 | } 78 | 79 | return PaginationAdapter::make($adapterConfiguration, $this->variableParser); 80 | } 81 | ); 82 | } 83 | 84 | private function setOrderRule(): void 85 | { 86 | $this->setRule( 87 | OrderAdapter::class, 88 | function (string $adapterType, array $adapterConfiguration) { 89 | if ($adapterType !== 'order') { 90 | return null; 91 | } 92 | 93 | return OrderAdapter::make($adapterConfiguration, $this->variableParser); 94 | } 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Stitcher/Page/PageParser.php: -------------------------------------------------------------------------------- 1 | pageFactory = $pageFactory; 23 | $this->adapterFactory = $adapterFactory; 24 | } 25 | 26 | public static function make(PageFactory $factory, AdapterFactory $adapterFactory): PageParser 27 | { 28 | return new self($factory, $adapterFactory); 29 | } 30 | 31 | public function parse($pageConfiguration): PageCollection 32 | { 33 | $result = new PageCollection(); 34 | 35 | $pageEntries = $this->parseAdapterConfiguration($pageConfiguration); 36 | 37 | foreach ($pageEntries as $pageEntry) { 38 | $page = $this->parsePage($pageEntry); 39 | 40 | $this->setCurrentPage($page); 41 | 42 | $result[$page->id()] = $page; 43 | } 44 | 45 | return $result; 46 | } 47 | 48 | public function getCurrentPage(): ?Page 49 | { 50 | return $this->currentPage; 51 | } 52 | 53 | private function setCurrentPage(Page $page): PageParser 54 | { 55 | $this->currentPage = $page; 56 | 57 | return $this; 58 | } 59 | 60 | private function parseAdapterConfiguration(array $pageConfiguration): array 61 | { 62 | $pageEntries = [$pageConfiguration]; 63 | 64 | $adapters = 65 | $pageConfiguration['config'] 66 | ?? $pageConfiguration['adapters'] 67 | ?? []; 68 | 69 | foreach ($adapters as $adapterType => $adapterConfiguration) { 70 | $adapter = $this->adapterFactory->create( 71 | $adapterType, 72 | $adapterConfiguration 73 | ); 74 | 75 | if (! $adapter) { 76 | throw InvalidConfiguration::adapterNotFound($adapterType); 77 | } 78 | 79 | $pageEntries = $this->adaptPageEntries($pageEntries, $adapter); 80 | } 81 | 82 | return $pageEntries; 83 | } 84 | 85 | private function adaptPageEntries(array $pageEntries, Adapter $adapter): array 86 | { 87 | $adaptedPageEntries = []; 88 | 89 | foreach ($pageEntries as $pageToTransform) { 90 | $adaptedPageEntries = array_merge( 91 | $adaptedPageEntries, 92 | $adapter->transform($pageToTransform) 93 | ); 94 | } 95 | 96 | $pageEntries = $adaptedPageEntries; 97 | 98 | return $pageEntries; 99 | } 100 | 101 | private function parsePage($inputConfiguration): Page 102 | { 103 | return $this->pageFactory->create($inputConfiguration); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Stitcher/Task/AbstractParse.php: -------------------------------------------------------------------------------- 1 | publicDirectory = rtrim($publicDirectory, '/'); 41 | $this->configurationFile = $configurationFile; 42 | $this->pageParser = $pageParser; 43 | $this->pageRenderer = $pageRenderer; 44 | $this->siteMap = $siteMap; 45 | } 46 | 47 | public static function make( 48 | string $publicDirectory, 49 | string $configurationFile, 50 | PageParser $pageParser, 51 | PageRenderer $pageRenderer, 52 | SiteMap $siteMap 53 | ) : self { 54 | return new static( 55 | $publicDirectory, 56 | $configurationFile, 57 | $pageParser, 58 | $pageRenderer, 59 | $siteMap 60 | ); 61 | } 62 | 63 | public function addSubTask(Task $task) 64 | { 65 | $this->tasks[] = $task; 66 | 67 | return $this; 68 | } 69 | 70 | protected function parsePageConfiguration($config): PageCollection 71 | { 72 | $pages = []; 73 | 74 | foreach ($config as $pageId => $pageConfiguration) { 75 | $pageConfiguration['id'] = $pageConfiguration['id'] ?? $pageId; 76 | 77 | $pages += $this->pageParser->parse($pageConfiguration)->toArray(); 78 | } 79 | 80 | return new PageCollection($pages); 81 | } 82 | 83 | protected function renderPages(PageCollection $pages): void 84 | { 85 | $fs = new Filesystem(); 86 | 87 | $pages->each(function (Page $page, string $pageId) use ($fs) { 88 | $fileName = $pageId === '/' ? 'index' : $pageId; 89 | 90 | $renderedPage = $this->pageRenderer->render($page); 91 | 92 | $this->siteMap->addPath($pageId); 93 | 94 | $fs->dumpFile( 95 | "{$this->publicDirectory}/{$fileName}.html", 96 | $renderedPage 97 | ); 98 | }); 99 | } 100 | 101 | protected function executeSubTasks(): void 102 | { 103 | foreach ($this->tasks as $task) { 104 | $task->execute(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/CreateStitcherObjects.php: -------------------------------------------------------------------------------- 1 | createVariableParser(File::path()); 33 | 34 | return PageParser::make( 35 | PageFactory::make($variableParser), 36 | AdapterFactory::make($variableParser) 37 | ); 38 | } 39 | 40 | protected function createVariableParser(string $sourceDirectory = null) : VariableParser 41 | { 42 | return VariableParser::make( 43 | VariableFactory::make() 44 | ->setMarkdownParser($this->createMarkdownParser()) 45 | ->setYamlParser(new Yaml()) 46 | ->setImageParser($this->createImageFactory($sourceDirectory)) 47 | ); 48 | } 49 | 50 | protected function createPageFactory(VariableParser $variableParser) : PageFactory 51 | { 52 | return PageFactory::make($variableParser); 53 | } 54 | 55 | protected function createAdapterFactory(VariableParser $variableParser) : AdapterFactory 56 | { 57 | return AdapterFactory::make($variableParser); 58 | } 59 | 60 | protected function createImageFactory($sourceDirectory = null): ImageFactory 61 | { 62 | $sourceDirectory = $sourceDirectory ?? File::path(); 63 | $publicPath = File::path('public'); 64 | 65 | return ImageFactory::make($sourceDirectory, $publicPath, FixedWidthScaler::make([ 66 | 300, 500, 67 | ])); 68 | } 69 | 70 | protected function createVariableFactory(VariableParser $variableParser = null) : VariableFactory 71 | { 72 | $variableParser = $variableParser ?? $this->createVariableParser(); 73 | 74 | $factory = VariableFactory::make() 75 | ->setVariableParser($variableParser) 76 | ->setMarkdownParser($this->createMarkdownParser()) 77 | ->setYamlParser(new Yaml()) 78 | ->setImageParser($this->createImageFactory()); 79 | 80 | return $factory; 81 | } 82 | 83 | protected function createMarkdownParser(): MarkdownParser 84 | { 85 | return new MarkdownParser($this->createImageFactory()); 86 | } 87 | 88 | protected function createSiteMap(): SiteMap 89 | { 90 | return new SiteMap('https://www.stitcher.io'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Stitcher/Page/Adapter/OrderAdapter.php: -------------------------------------------------------------------------------- 1 | isValidConfiguration($adapterConfiguration)) { 37 | throw InvalidOrderAdapter::create(); 38 | } 39 | 40 | $this->variable = $adapterConfiguration['variable']; 41 | $this->field = $adapterConfiguration['field']; 42 | $this->direction = $adapterConfiguration['direction'] ?? 'asc'; 43 | 44 | $this->variableParser = $variableParser; 45 | } 46 | 47 | public static function make( 48 | array $adapterConfiguration, 49 | VariableParser $variableParser 50 | ): OrderAdapter { 51 | return new self($adapterConfiguration, $variableParser); 52 | } 53 | 54 | public function transform(array $pageConfiguration): array 55 | { 56 | $entries = $this->getEntries($pageConfiguration); 57 | 58 | $orderedEntries = $this->orderEntries($entries); 59 | 60 | $pageConfiguration['variables'][$this->variable] = $orderedEntries; 61 | 62 | unset($pageConfiguration['config']['order']); 63 | 64 | return [$pageConfiguration]; 65 | } 66 | 67 | public function isValidConfiguration($subject): bool 68 | { 69 | return \is_array($subject) 70 | && isset($subject['variable']) 71 | && isset($subject['field']); 72 | } 73 | 74 | private function getEntries($pageConfiguration): ?array 75 | { 76 | $variable = $pageConfiguration['variables'][$this->variable] ?? null; 77 | 78 | if (is_array($variable)) { 79 | return $variable; 80 | } 81 | 82 | $entries = Yaml::parse(File::read($variable)); 83 | 84 | foreach ($entries as $id => $data) { 85 | $data['id'] = $data['id'] ?? $id; 86 | 87 | $entries[$id] = $data; 88 | } 89 | 90 | return $entries; 91 | } 92 | 93 | private function orderEntries(array $entries): array 94 | { 95 | uasort($entries, function ($a, $b) { 96 | return strcmp($a[$this->field] ?? '', $b[$this->field] ?? ''); 97 | }); 98 | 99 | if (in_array($this->direction, self::REVERSE)) { 100 | $entries = array_reverse($entries, true); 101 | } 102 | 103 | return $entries; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Stitcher/Application/Router.php: -------------------------------------------------------------------------------- 1 | routeCollector = $routeCollector; 24 | } 25 | 26 | public function get(string $url, string $controller): Router 27 | { 28 | $this->routeCollector->addRoute('GET', $url, [$controller, 'handle']); 29 | 30 | return $this; 31 | } 32 | 33 | public function put(string $url, string $controller): Router 34 | { 35 | $this->routeCollector->addRoute('PUT', $url, [$controller, 'handle']); 36 | 37 | return $this; 38 | } 39 | 40 | public function post(string $url, string $controller): Router 41 | { 42 | $this->routeCollector->addRoute('POST', $url, [$controller, 'handle']); 43 | 44 | return $this; 45 | } 46 | 47 | public function patch(string $url, string $controller): Router 48 | { 49 | $this->routeCollector->addRoute('PATCH', $url, [$controller, 'handle']); 50 | 51 | return $this; 52 | } 53 | 54 | public function delete(string $url, string $controller): Router 55 | { 56 | $this->routeCollector->addRoute('DELETE', $url, [$controller, 'handle']); 57 | 58 | return $this; 59 | } 60 | 61 | public function redirect(string $url, string $targetUrl): Router 62 | { 63 | $this->redirects[$url] = $targetUrl; 64 | 65 | return $this; 66 | } 67 | 68 | public function getRedirectForUrl(string $url): ?string 69 | { 70 | return $this->redirects[$url] ?? null; 71 | } 72 | 73 | public function dispatch(Request $request): ?Response 74 | { 75 | $dispatcher = new GroupCountBased($this->routeCollector->getData()); 76 | 77 | $routeInfo = $dispatcher->dispatch( 78 | $request->getMethod(), 79 | $request->getUri()->getPath() 80 | ); 81 | 82 | if ($routeInfo[0] !== Dispatcher::FOUND) { 83 | return null; 84 | } 85 | 86 | $handler = $this->resolveHandler($routeInfo[1]); 87 | $parameters = $routeInfo[2]; 88 | 89 | return \call_user_func_array( 90 | $handler, 91 | array_merge([$request], $parameters) 92 | ); 93 | } 94 | 95 | protected function resolveHandler(array $callback): array 96 | { 97 | $className = $callback[0]; 98 | 99 | try { 100 | $handler = App::get($className); 101 | } catch (ServiceNotFoundException $e) { 102 | $handler = new $className(); 103 | } 104 | 105 | $callback[0] = $handler; 106 | 107 | return $callback; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Stitcher/Exception/InvalidConfiguration.php: -------------------------------------------------------------------------------- 1 | File::path('src/site.yaml'), 24 | ]; 25 | ``` 26 | 27 | This `site.yaml` file contains a list of routes and their configuration. 28 | 29 | ```yaml 30 | /: 31 | template: home.twig 32 | # ... 33 | 34 | /blog/page-{page}: 35 | template: blog/overview.twig 36 | # ... 37 | 38 | /blog/{id}: 39 | template: blog/detail.twig 40 | # ... 41 | ``` 42 | MD 43 | 44 | ); 45 | } 46 | 47 | public static function pageTemplateMissing(string $pageId): InvalidConfiguration 48 | { 49 | $templateDirectory = File::relativePath(Config::get('templateDirectory')); 50 | 51 | return new self( 52 | 'A page requires a `template` value.', 53 | << File::path('resources/view'), 69 | ]; 70 | ``` 71 | MD 72 | 73 | ); 74 | } 75 | 76 | public static function pageIdMissing($pageConfiguration): InvalidConfiguration 77 | { 78 | $yaml = Yaml::dump($pageConfiguration); 79 | 80 | return new self('A page requires an `id` value.', <<load(); 26 | } catch (InvalidPathException $e) { 27 | throw InvalidConfiguration::dotEnvNotFound(File::path()); 28 | } 29 | 30 | $loadedConfiguration = []; 31 | 32 | if (is_dir(File::path('config'))) { 33 | $configurationFiles = Finder::create()->files()->in(File::path('config'))->name('*.php')->getIterator(); 34 | 35 | $loadedConfiguration = array_merge($loadedConfiguration, self::load($configurationFiles)); 36 | } 37 | 38 | if (file_exists(File::path('src/config.php'))) { 39 | $sourceConfigurationFile = Finder::create()->files()->in(File::path('src'))->name('config.php')->getIterator(); 40 | 41 | $loadedConfiguration = array_merge($loadedConfiguration, self::load($sourceConfigurationFile)); 42 | } 43 | 44 | self::registerPlugins($loadedConfiguration); 45 | 46 | self::registerConfiguration($loadedConfiguration); 47 | } 48 | 49 | public static function get(string $key) 50 | { 51 | return self::$loadedConfiguration[$key] ?? null; 52 | } 53 | 54 | public static function all(): array 55 | { 56 | return self::$loadedConfiguration; 57 | } 58 | 59 | public static function plugins(): array 60 | { 61 | return self::$plugins; 62 | } 63 | 64 | protected static function defaults(): array 65 | { 66 | return [ 67 | 'rootDirectory' => File::path(), 68 | 'resourcesPath' => File::path('resources'), 69 | 'templateRenderer' => 'twig', 70 | 'staticFiles' => [], 71 | 'cacheStaticFiles' => false, 72 | 'cacheImages' => true, 73 | 'siteUrl' => '', 74 | 'errorPages' => [], 75 | 'minify' => false, 76 | ]; 77 | } 78 | 79 | protected static function load(Iterator $configurationFiles): array 80 | { 81 | $loadedConfiguration = []; 82 | 83 | foreach ($configurationFiles as $configurationFile) { 84 | $loadedFileConfiguration = require $configurationFile; 85 | 86 | if (! \is_array($loadedFileConfiguration)) { 87 | continue; 88 | } 89 | 90 | $loadedConfiguration = array_merge($loadedConfiguration, $loadedFileConfiguration); 91 | } 92 | 93 | return $loadedConfiguration; 94 | } 95 | 96 | protected static function registerPlugins(array $loadedConfiguration): void 97 | { 98 | self::$plugins = $loadedConfiguration['plugins'] ?? []; 99 | } 100 | 101 | protected static function registerConfiguration(array $loadedConfiguration): void 102 | { 103 | self::$loadedConfiguration = array_merge( 104 | self::defaults(), 105 | $loadedConfiguration, 106 | Arr::dot($loadedConfiguration) 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/Stitcher/Page/Adapter/PaginationAdapterTest.php: -------------------------------------------------------------------------------- 1 | createPageConfiguration(); 17 | 18 | $adapter = PaginationAdapter::make($pageConfiguration['config']['pagination'], $this->createVariableParser()); 19 | $result = $adapter->transform($pageConfiguration); 20 | 21 | $this->assertCount(3, $result); 22 | 23 | $this->assertCount(1, $result['/page-1']['variables']['entries']); 24 | $this->assertEquals('A', $result['/page-1']['variables']['entries']['a']['name']); 25 | 26 | $this->assertCount(1, $result['/page-2']['variables']['entries']); 27 | $this->assertEquals('B', $result['/page-2']['variables']['entries']['b']['name']); 28 | 29 | $this->assertCount(1, $result['/page-3']['variables']['entries']); 30 | $this->assertEquals('C', $result['/page-3']['variables']['entries']['c']['name']); 31 | } 32 | 33 | /** @test */ 34 | public function it_sets_the_pagination_variable(): void 35 | { 36 | $pageConfiguration = $this->createPageConfiguration(); 37 | 38 | $adapter = PaginationAdapter::make($pageConfiguration['config']['pagination'], $this->createVariableParser()); 39 | $result = $adapter->transform($pageConfiguration); 40 | 41 | $page1 = $result['/page-1']; 42 | $this->assertTrue(isset($page1['variables']['_pagination'])); 43 | $this->assertEquals(2, $page1['variables']['_pagination']['next']['index']); 44 | $this->assertEquals('/page-2', $page1['variables']['_pagination']['next']['url']); 45 | $this->assertNull($page1['variables']['_pagination']['previous']); 46 | 47 | $page2 = $result['/page-2']; 48 | $this->assertTrue(isset($page2['variables']['_pagination'])); 49 | $this->assertEquals(1, $page2['variables']['_pagination']['previous']['index']); 50 | $this->assertEquals('/page-1', $page2['variables']['_pagination']['previous']['url']); 51 | $this->assertEquals(3, $page2['variables']['_pagination']['next']['index']); 52 | $this->assertEquals('/page-3', $page2['variables']['_pagination']['next']['url']); 53 | 54 | $page3 = $result['/page-3']; 55 | $this->assertTrue(isset($page3['variables']['_pagination'])); 56 | $this->assertEquals(2, $page3['variables']['_pagination']['previous']['index']); 57 | $this->assertEquals('/page-2', $page3['variables']['_pagination']['previous']['url']); 58 | $this->assertNull($page3['variables']['_pagination']['next']); 59 | } 60 | 61 | private function createPageConfiguration(): array 62 | { 63 | File::write('entries.yaml', << '/page-{page}', 78 | 'template' => 'index.twig', 79 | 'variables' => [ 80 | 'entries' => 'entries.yaml', 81 | ], 82 | 'config' => [ 83 | 'pagination' => [ 84 | 'variable' => 'entries', 85 | 'perPage' => 1, 86 | 'parameter' => 'page', 87 | ], 88 | ], 89 | ]; 90 | 91 | return $pageConfiguration; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Stitcher/Integration/FullSiteParseTest.php: -------------------------------------------------------------------------------- 1 | execute(); 19 | 20 | $this->assertIndexPageParsed(); 21 | $this->assertOverviewPageParsed(); 22 | $this->assertOverviewPaginatedPageParsed(); 23 | $this->assertDetailPageParsed(); 24 | $this->assertImageParsed(); 25 | $this->assertCss(); 26 | $this->assertJs(); 27 | } 28 | 29 | private function assertIndexPageParsed(): void 30 | { 31 | $html = File::read('public/index.html'); 32 | 33 | $this->assertNotNull($html); 34 | $this->assertContains('', $html); 35 | } 36 | 37 | private function assertOverviewPageParsed(): void 38 | { 39 | $html = File::read('public/entries.html'); 40 | 41 | $this->assertNotNull($html); 42 | $this->assertContains('

A

', $html); 43 | $this->assertContains('

B

', $html); 44 | $this->assertContains('

C

', $html); 45 | } 46 | 47 | private function assertOverviewPaginatedPageParsed(): void 48 | { 49 | $page1 = File::read('public/entries/page-1.html'); 50 | $this->assertNotNull($page1); 51 | $this->assertContains('

A

', $page1); 52 | $this->assertContains('

B

', $page1); 53 | $this->assertNotContains('

C

', $page1); 54 | $this->assertNotContains('assertContains('assertContains('assertNotNull($page2); 60 | $this->assertContains('

C

', $page2); 61 | $this->assertContains('
assertContains('assertContains('assertContains('assertNotNull($page3); 68 | } 69 | 70 | private function assertDetailPageParsed(): void 71 | { 72 | $detail = File::read('public/entries/a.html'); 73 | $this->assertNotNull($detail); 74 | $this->assertContains('

A

', $detail); 75 | } 76 | 77 | private function assertImageParsed(): void 78 | { 79 | $detail = File::read('public/entries/a.html'); 80 | 81 | $this->assertContains('assertContains('srcset="/resources/images/green-', $detail); 83 | $this->assertContains('/resources/images/green-250x250.jpg', $detail); 84 | $this->assertContains('alt="test"', $detail); 85 | } 86 | 87 | private function assertCss(): void 88 | { 89 | $detail = File::read('public/index.html'); 90 | 91 | $this->assertContains('', $detail); 92 | $this->assertContains('', $detail); 94 | $this->assertContains('body {', $detail); 95 | } 96 | 97 | private function assertJs(): void 98 | { 99 | $detail = File::read('public/index.html'); 100 | 101 | $this->assertContains('', $detail); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Stitcher/Application/Server.php: -------------------------------------------------------------------------------- 1 | router = $router; 32 | 33 | return $this; 34 | } 35 | 36 | public function setHeaderContainer(HeaderContainer $headerContainer): Server 37 | { 38 | $this->headerContainer = $headerContainer; 39 | 40 | return $this; 41 | } 42 | 43 | public function setErrorHandler(ErrorHandler $errorHandler): Server 44 | { 45 | $this->errorHandler = $errorHandler; 46 | 47 | return $this; 48 | } 49 | 50 | public function run(): string 51 | { 52 | $response = $this->createResponse(); 53 | 54 | return $this->handleResponse($response); 55 | } 56 | 57 | protected function getRequest(): Request 58 | { 59 | if (! $this->request) { 60 | $this->request = ServerRequest::fromGlobals(); 61 | } 62 | 63 | return $this->request; 64 | } 65 | 66 | protected function getCurrentPath(): string 67 | { 68 | $path = $this->getRequest()->getUri()->getPath(); 69 | 70 | return $path === '' ? '/' : $path; 71 | } 72 | 73 | protected function handleDynamicRoute(): ?Response 74 | { 75 | if (! $this->router) { 76 | return null; 77 | } 78 | 79 | return $this->router->dispatch($this->getRequest()); 80 | } 81 | 82 | protected function createResponse(): Response 83 | { 84 | if ( 85 | $this->router 86 | && $redirectTo = $this->router->getRedirectForUrl($this->getCurrentPath()) 87 | ) { 88 | return $this->createRedirectResponse($redirectTo); 89 | } 90 | 91 | try { 92 | $response = $this->handleStaticRoute(); 93 | 94 | if (! $response) { 95 | $response = $this->handleDynamicRoute(); 96 | } 97 | } catch (StitcherException $e) { 98 | $response = $this->createErrorResponse($e); 99 | } 100 | 101 | return $response ?? $this->createErrorResponse( 102 | Http::notFound( 103 | $this->getCurrentPath() 104 | ) 105 | ); 106 | } 107 | 108 | protected function createRedirectResponse(string $targetUrl): Response 109 | { 110 | return new Response(301, ['Location' => $targetUrl]); 111 | } 112 | 113 | protected function createErrorResponse(StitcherException $exception): Response 114 | { 115 | $statusCode = $exception instanceof Http ? $exception->statusCode() : 500; 116 | 117 | $responseBody = $this->errorHandler->handle($statusCode, $exception); 118 | 119 | return new Response($statusCode, [], $responseBody); 120 | } 121 | 122 | protected function handleResponse(Response $response): string 123 | { 124 | foreach ($response->getHeaders() as $name => $headers) { 125 | header($name . ':'. implode(', ', $headers)); 126 | } 127 | 128 | if ($this->headerContainer) { 129 | foreach ($this->headerContainer as $header) { 130 | header((string) $header); 131 | } 132 | } 133 | 134 | http_response_code($response->getStatusCode()); 135 | 136 | return $response->getBody()->getContents(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Pageon/Html/Meta/Meta.php: -------------------------------------------------------------------------------- 1 | charset($charset); 24 | $this->name('viewport', 'width=device-width, initial-scale=1'); 25 | 26 | $this->socialMeta = [ 27 | new GooglePlusMeta($this), 28 | new TwitterMeta($this), 29 | new OpenGraphMeta($this), 30 | ]; 31 | } 32 | 33 | public static function create(string $charset = 'UTF-8') : Meta { 34 | return new self($charset); 35 | } 36 | 37 | public function render(array $extra = []) : string { 38 | $html = ''; 39 | 40 | /** 41 | * @var string $type 42 | * @var MetaItem[] $metaItems 43 | */ 44 | foreach ($this->meta as $type => $metaItems) { 45 | foreach ($metaItems as $metaItem) { 46 | $html .= $metaItem->render($extra) . "\n"; 47 | } 48 | } 49 | 50 | return $html; 51 | } 52 | 53 | public function charset(string $charset) : Meta { 54 | $item = CharsetMeta::create($charset); 55 | $this->meta['charset'][] = $item; 56 | 57 | return $this; 58 | } 59 | 60 | public function name(string $name, ?string $content) : Meta { 61 | if (!$content) { 62 | return $this; 63 | } 64 | 65 | $item = NameMeta::create($name, $content); 66 | $this->meta['name'][$name] = $item; 67 | 68 | return $this; 69 | } 70 | 71 | public function itemprop(string $name, ?string $content) : Meta { 72 | if (!$content) { 73 | return $this; 74 | } 75 | 76 | $item = ItemPropMeta::create($name, $content); 77 | $this->meta['itemprop'][$name] = $item; 78 | 79 | return $this; 80 | } 81 | 82 | public function property(string $property, ?string $content) : Meta { 83 | if (!$content) { 84 | return $this; 85 | } 86 | 87 | $item = PropertyMeta::create($property, $content); 88 | $this->meta['property'][$property] = $item; 89 | 90 | return $this; 91 | } 92 | 93 | public function httpEquiv(string $httpEquiv, ?string $content) : Meta { 94 | if (!$content) { 95 | return $this; 96 | } 97 | 98 | $item = HttpEquivMeta::create($httpEquiv, $content); 99 | $this->meta['httpEquiv'][$httpEquiv] = $item; 100 | 101 | return $this; 102 | } 103 | 104 | public function link(string $rel, ?string $href) : Meta { 105 | if (!$href) { 106 | return $this; 107 | } 108 | 109 | $item = LinkMeta::create($rel, $href); 110 | $this->meta['link'][$rel] = $item; 111 | 112 | return $this; 113 | } 114 | 115 | public function title(?string $content) : Meta { 116 | if (!$content) { 117 | return $this; 118 | } 119 | 120 | $this->name('title', $content); 121 | 122 | foreach ($this->socialMeta as $socialMeta) { 123 | $socialMeta->title($content); 124 | } 125 | 126 | return $this; 127 | } 128 | 129 | public function description(?string $content) : Meta { 130 | if (!$content) { 131 | return $this; 132 | } 133 | 134 | $this->name('description', $content); 135 | 136 | foreach ($this->socialMeta as $socialMeta) { 137 | $socialMeta->description($content); 138 | } 139 | 140 | return $this; 141 | } 142 | 143 | public function image(?string $content) : Meta { 144 | if (!$content) { 145 | return $this; 146 | } 147 | 148 | $this->name('image', $content); 149 | 150 | foreach ($this->socialMeta as $socialMeta) { 151 | $socialMeta->image($content); 152 | } 153 | 154 | return $this; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Stitcher/Page/Adapter/PaginationAdapter.php: -------------------------------------------------------------------------------- 1 | isValidConfiguration($adapterConfiguration)) { 29 | throw InvalidPaginationAdapter::create(); 30 | } 31 | 32 | $this->variable = $adapterConfiguration['variable']; 33 | $this->parameter = $adapterConfiguration['parameter']; 34 | $this->perPage = $adapterConfiguration['perPage'] ?? 12; 35 | $this->variableParser = $variableParser; 36 | } 37 | 38 | public static function make( 39 | array $adapterConfiguration, 40 | VariableParser $variableParser 41 | ): PaginationAdapter { 42 | return new self($adapterConfiguration, $variableParser); 43 | } 44 | 45 | public function transform(array $pageConfiguration): array 46 | { 47 | $paginationPageConfiguration = []; 48 | $entries = $this->getEntries($pageConfiguration); 49 | $pageCount = (int) ceil(\count($entries) / $this->perPage); 50 | 51 | for ($pageIndex = 1; $pageIndex <= $pageCount; $pageIndex++) { 52 | $entriesForPage = array_splice($entries, 0, $this->perPage); 53 | 54 | $entryConfiguration = $this->createPageConfiguration( 55 | $pageConfiguration, 56 | $entriesForPage, 57 | $pageIndex, 58 | $pageCount 59 | ); 60 | 61 | $paginationPageConfiguration[$entryConfiguration['id']] = $entryConfiguration; 62 | } 63 | 64 | return $paginationPageConfiguration; 65 | } 66 | 67 | public function isValidConfiguration($subject): bool 68 | { 69 | return 70 | \is_array($subject) 71 | && isset($subject['variable']) 72 | && isset($subject['parameter']); 73 | } 74 | 75 | protected function getEntries(array $pageConfiguration): ?array 76 | { 77 | $variable = $pageConfiguration['variables'][$this->variable] ?? null; 78 | $entries = $this->variableParser->parse($variable)['entries'] 79 | ?? $this->variableParser->parse($variable) 80 | ?? $variable; 81 | 82 | return $entries; 83 | } 84 | 85 | protected function createPageConfiguration( 86 | array $entryConfiguration, 87 | array $entriesForPage, 88 | int $pageIndex, 89 | int $pageCount 90 | ): array { 91 | $pageId = rtrim($entryConfiguration['id'], '/'); 92 | $paginatedId = $this->createPaginatedUrl($pageId, $pageIndex); 93 | 94 | $entryConfiguration['id'] = $paginatedId; 95 | $entryConfiguration['variables'][$this->variable] = $entriesForPage; 96 | 97 | $paginationVariable = $this->createPaginationVariable($pageId, $pageIndex, $pageCount); 98 | $entryConfiguration['variables']['_pagination'] = $paginationVariable; 99 | 100 | unset($entryConfiguration['config']['pagination']); 101 | 102 | return $entryConfiguration; 103 | } 104 | 105 | protected function createPaginationVariable( 106 | string $pageId, 107 | int $pageIndex, 108 | int $pageCount 109 | ): array { 110 | return [ 111 | 'current' => $pageIndex, 112 | 'previous' => $this->createPreviousPagination($pageId, $pageIndex), 113 | 'next' => $this->createNextPagination($pageId, $pageIndex, $pageCount), 114 | 'pages' => $pageCount, 115 | ]; 116 | } 117 | 118 | protected function createPreviousPagination( 119 | string $pageId, 120 | int $pageIndex 121 | ): ?array { 122 | if ($pageIndex <= 1) { 123 | return null; 124 | } 125 | 126 | $previous = $pageIndex - 1; 127 | 128 | return [ 129 | 'url' => $this->createPaginatedUrl($pageId, $previous), 130 | 'index' => $previous, 131 | ]; 132 | } 133 | 134 | protected function createNextPagination( 135 | string $pageId, 136 | int $pageIndex, 137 | int $pageCount 138 | ): ?array { 139 | if ($pageIndex >= $pageCount) { 140 | return null; 141 | } 142 | 143 | $next = $pageIndex + 1; 144 | 145 | return [ 146 | 'url' => $this->createPaginatedUrl($pageId, $next), 147 | 'index' => $next, 148 | ]; 149 | } 150 | 151 | protected function createPaginatedUrl(string $pageId, int $index): string 152 | { 153 | return str_replace('{' .$this->parameter. '}', $index, $pageId); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Stitcher/Variable/VariableFactory.php: -------------------------------------------------------------------------------- 1 | setJsonRule(); 28 | $this->setYamlRule(); 29 | $this->setMarkdownRule(); 30 | $this->setImageRule(); 31 | $this->setDirectoryRule(); 32 | $this->setHtmlRule(); 33 | } 34 | 35 | public static function make(): VariableFactory 36 | { 37 | return new self(); 38 | } 39 | 40 | public function setYamlParser(Yaml $yamlParser): VariableFactory 41 | { 42 | $this->yamlParser = $yamlParser; 43 | 44 | return $this; 45 | } 46 | 47 | public function setMarkdownParser(MarkdownParser $markdownParser): VariableFactory 48 | { 49 | $this->markdownParser = $markdownParser; 50 | 51 | return $this; 52 | } 53 | 54 | public function setImageParser(ImageFactory $imageParser): VariableFactory 55 | { 56 | $this->imageParser = $imageParser; 57 | 58 | return $this; 59 | } 60 | 61 | public function setVariableParser(VariableParser $variableParser): VariableFactory 62 | { 63 | $this->variableParser = $variableParser; 64 | 65 | return $this; 66 | } 67 | 68 | public function create($value): AbstractVariable 69 | { 70 | foreach ($this->getRules() as $rule) { 71 | try { 72 | $variable = $rule($value); 73 | } catch (TypeError $e) { 74 | continue; 75 | } 76 | 77 | if ($variable instanceof AbstractVariable) { 78 | return $variable; 79 | } 80 | } 81 | 82 | return DefaultVariable::make($value); 83 | } 84 | 85 | private function setJsonRule(): DynamicFactory 86 | { 87 | return $this->setRule(JsonVariable::class, function (string $value) { 88 | if (pathinfo($value, PATHINFO_EXTENSION) !== 'json') { 89 | return null; 90 | } 91 | 92 | return JsonVariable::make($value); 93 | }); 94 | } 95 | 96 | private function setYamlRule(): void 97 | { 98 | $this->setRule(YamlVariable::class, function (string $value) { 99 | if (! $this->yamlParser) { 100 | return null; 101 | } 102 | 103 | $extension = pathinfo($value, PATHINFO_EXTENSION); 104 | 105 | if (! \in_array($extension, ['yaml', 'yml'])) { 106 | return null; 107 | } 108 | 109 | return YamlVariable::make($value, $this->yamlParser, $this->variableParser); 110 | }); 111 | } 112 | 113 | private function setMarkdownRule(): void 114 | { 115 | $this->setRule(MarkdownVariable::class, function (string $value) { 116 | if (! $this->markdownParser) { 117 | return null; 118 | } 119 | 120 | if (pathinfo($value, PATHINFO_EXTENSION) !== 'md') { 121 | return null; 122 | } 123 | 124 | return MarkdownVariable::make($value, $this->markdownParser); 125 | }); 126 | } 127 | 128 | private function setHtmlRule(): void 129 | { 130 | $this->setRule(HtmlVariable::class, function (string $value) { 131 | if (pathinfo($value, PATHINFO_EXTENSION) !== 'html') { 132 | return null; 133 | } 134 | 135 | return new HtmlVariable($value); 136 | }); 137 | } 138 | 139 | private function setImageRule(): void 140 | { 141 | $this->setRule(ImageVariable::class, function ($value) { 142 | if (! $this->imageParser) { 143 | return null; 144 | } 145 | 146 | $srcPath = \is_array($value) ? $value['src'] ?? null : $value; 147 | 148 | $extension = pathinfo($srcPath, PATHINFO_EXTENSION); 149 | 150 | if (! \in_array($extension, ['jpeg', 'jpg', 'png', 'gif'])) { 151 | return null; 152 | } 153 | 154 | return ImageVariable::make($value, $this->imageParser); 155 | }); 156 | } 157 | 158 | private function setDirectoryRule(): void 159 | { 160 | $this->setRule(DirectoryVariable::class, function ($value) { 161 | if (!is_string($value) || substr($value, -1) !== '/') { 162 | return null; 163 | } 164 | 165 | if (strpos($value, 'http') !== false) { 166 | return null; 167 | } 168 | 169 | return new DirectoryVariable($value, $this->variableParser); 170 | }); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Stitcher/App.php: -------------------------------------------------------------------------------- 1 | get($id); 47 | } 48 | 49 | public static function developmentServer(): DevelopmentServer 50 | { 51 | try { 52 | return self::get(DevelopmentServer::class); 53 | } catch (ParameterNotFoundException $e) { 54 | throw InvalidConfiguration::missingParameter($e->getKey()); 55 | } 56 | } 57 | 58 | public static function productionServer(): ProductionServer 59 | { 60 | try { 61 | return self::get(ProductionServer::class); 62 | } catch (ParameterNotFoundException $e) { 63 | throw InvalidConfiguration::missingParameter($e->getKey()); 64 | } 65 | } 66 | 67 | public static function router(): Router 68 | { 69 | return self::get(Router::class); 70 | } 71 | 72 | protected static function loadConfig(array $config): void 73 | { 74 | foreach ($config as $key => $value) { 75 | self::$container->setParameter($key, $value); 76 | } 77 | } 78 | 79 | protected static function loadServices(string $servicesPath): void 80 | { 81 | $loader = new YamlFileLoader(self::$container, new FileLocator(__DIR__)); 82 | 83 | $loader->load($servicesPath); 84 | 85 | /** @var Definition $definition */ 86 | foreach (self::$container->getDefinitions() as $id => $definition) { 87 | if (! $definition->getClass()) { 88 | continue; 89 | } 90 | 91 | self::$container->setAlias($definition->getClass(), $id); 92 | } 93 | } 94 | 95 | protected static function loadPlugins(): void 96 | { 97 | foreach (Config::plugins() as $pluginClass) { 98 | if (!class_implements($pluginClass, Plugin::class)) { 99 | throw InvalidPlugin::doesntImplementPluginInterface($pluginClass); 100 | } 101 | 102 | self::loadPluginConfiguration($pluginClass); 103 | 104 | self::loadPluginServices($pluginClass); 105 | 106 | self::registerPluginDefinition($pluginClass); 107 | } 108 | } 109 | 110 | protected static function loadPluginConfiguration(string $pluginClass): void 111 | { 112 | $configurationPath = forward_static_call([$pluginClass, 'getConfigurationPath']); 113 | 114 | if (!$configurationPath) { 115 | return; 116 | } 117 | 118 | if (!file_exists($configurationPath)) { 119 | throw InvalidPlugin::configurationFileNotFound($pluginClass, $configurationPath); 120 | } 121 | 122 | $pluginConfiguration = require $configurationPath; 123 | 124 | if (! \is_array($pluginConfiguration)) { 125 | throw InvalidPlugin::configurationMustBeArray($pluginClass, $configurationPath); 126 | } 127 | 128 | self::loadConfig(Arr::dot($pluginConfiguration)); 129 | } 130 | 131 | protected static function loadPluginServices(string $pluginClass): void 132 | { 133 | $servicesPath = forward_static_call([$pluginClass, 'getServicesPath']); 134 | 135 | if (!$servicesPath) { 136 | return; 137 | } 138 | 139 | if (!file_exists($servicesPath)) { 140 | throw InvalidPlugin::serviceFileNotFound($pluginClass, $servicesPath); 141 | } 142 | 143 | self::loadServices($servicesPath); 144 | } 145 | 146 | protected static function registerPluginDefinition(string $pluginClass): void 147 | { 148 | $definition = new Definition($pluginClass); 149 | 150 | $definition->setAutowired(true); 151 | 152 | self::$container->setDefinition($pluginClass, $definition); 153 | 154 | forward_static_call([$pluginClass, 'boot']); 155 | } 156 | 157 | protected static function loadRoutes(): void 158 | { 159 | $routeFile = File::path('src/routes.php'); 160 | 161 | if (! file_exists($routeFile)) { 162 | return; 163 | } 164 | 165 | require_once $routeFile; 166 | } 167 | } 168 | --------------------------------------------------------------------------------