├── .gitignore ├── .travis.yml ├── README.md ├── bin └── soushi ├── composer.json ├── config.php ├── phpunit.xml ├── source ├── config.md ├── deployment.md ├── first.md ├── index.md ├── main.css ├── template.md └── variables.md ├── src ├── Aggregator.php ├── Cli.php ├── Command.php ├── Command │ ├── Base.php │ ├── Build.php │ ├── Ghpages.php │ └── Init.php ├── Config.php ├── Exception │ ├── File.php │ └── PageNotFound.php ├── File.php ├── File │ ├── Asset.php │ ├── Base.php │ ├── Factory.php │ └── Page.php ├── Parser.php ├── Template.php └── Web.php ├── templates └── page.php └── tests ├── AggregatorTest.php ├── Command ├── BuildTest.php ├── GhpagesTest.php └── InitTest.php ├── ConfigTest.php ├── File ├── AssetTest.php ├── FactoryTest.php └── PageTest.php ├── ParserTest.php ├── TemplateTest.php ├── WebTest.php └── assets ├── config.php ├── source ├── css │ └── main.css ├── index.md ├── js │ └── main.js └── subdir │ ├── bar.md │ └── foo.md └── templates ├── index.php ├── layout.php └── page.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | 4 | /build 5 | /public 6 | /tmp -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.1.0 4 | before_script: 5 | - composer install 6 | script: phpunit tests/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Soushi: Bimodal Site Generator Powered by PHP [![Build Status](https://travis-ci.org/kentaro/soushi.svg?branch=add-ci)](https://travis-ci.org/kentaro/soushi) 2 | 3 | Soushi (草紙 in Japanese) is a bimodal site generator powered by PHP. 4 | 5 | ## Concept 6 | 7 | In what way is Soushi bimodal? 8 | 9 | Soushi is bimodal in terms of that it supports both generating a static Web site for [GitHub Pages](https://pages.github.com/) and serving contents dynamically via PHP scripts which can be simply put onto Web server, making HTML files from source files written in Markdown. 10 | 11 | Soushi exploits the strength of PHP which allows us to easily deploy scripts and which is, itself, a template language, being enhanced by [Plates](http://platesphp.com/) library. 12 | 13 | ## Documentaion 14 | 15 | See http://kentarok.org/soushi/ for documentation. Which is also a live example of Soushi. 16 | 17 | ## Author 18 | 19 | Kentaro Kuribayashi 20 | 21 | ## License 22 | 23 | MIT 24 | -------------------------------------------------------------------------------- /bin/soushi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run($argv); 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kentaro/soushi", 3 | "type": "library", 4 | "description": "Bimodal Site Generator Powered by PHP.", 5 | "keywords": ["blog", "site", "generator"], 6 | "homepage": "http://kentarok.org/soushi/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Kentaro Kuribayashi", 11 | "email": "kentarok@gmail.com", 12 | "homepage": "http://kentarok.org/" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=7.1", 17 | "league/plates": "^3.1", 18 | "nategood/commando": "^0.2.9", 19 | "mnapoli/front-yaml": "^1.5", 20 | "symfony/finder": "^3.2" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "^5.7" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Soushi\\" : "src/" 28 | } 29 | }, 30 | "bin": [ 31 | "bin/soushi" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | "Soushi: Bimodal Site Generator Powered by PHP", 4 | "template_dir" => dirname(__FILE__) . "/templates", 5 | "source_dir" => dirname(__FILE__) . "/source", 6 | ]; 7 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /source/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | template: page 4 | --- 5 | 6 | You can set some variables used globally in your Web site using a configuration file. 7 | 8 | ### Configuration File 9 | 10 | To run `soushi init` command, `config.php` is generated in the root of your site directory. 11 | 12 | You can set any key/value pairs you like in the file. 13 | 14 | **config.php**: 15 | 16 | ```php 17 | "My Homepage", 20 | "template_dir" => dirname(__FILE__) . "/templates", 21 | "source_dir" => dirname(__FILE__) . "/source", 22 | ]; 23 | ``` 24 | 25 | As you see above, it's just a PHP associative-array. You have to notice that the value is `return`ed. Otherwise, the statement which loads the file doesn't return the configuration value as an array. 26 | 27 | ### In Templates 28 | 29 | The configurations in `config.php` is passed into templates as `$config` variable to enable you to get information from it. 30 | 31 | ```php 32 | <?= $config->site_title() ?> 33 | ``` 34 | 35 | `$config` actually is a `\Soushi\Config` object. You can call methods with key names in the configuration hash to retrieve values which are correspond to the keys. 36 | 37 | If you set some key/value pairs(eg. `"foo" => "bar"`), you can call `$config->foo()` to retrieve `"bar"`. 38 | -------------------------------------------------------------------------------- /source/deployment.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deployment 3 | template: page 4 | --- 5 | 6 | As already said, Soushi is bimodal. That is, you can choose from two methods to deploy your Web site; static Web site for GitHub Pages and dynamic Web site for Web server (ex. httpd). 7 | 8 | ### Static Web Site 9 | 10 | Soushi enables you to easily deploy your Web site to GitHub Pages. 11 | 12 | #### Generating Static Files 13 | 14 | Run `soushi build` to generate static files: 15 | 16 | ```sh 17 | $ ./vendor/bin/soushi build 18 | ``` 19 | 20 | Files are placed in `build/` directory like below: 21 | 22 | ``` 23 | build/ 24 | └─ main.css 25 | └─ index.html 26 | ``` 27 | 28 | #### Deployment 29 | 30 | Run `soushi gh-pages` command to deploy the site to GitHub Pages. 31 | 32 | ```sh 33 | $ ./vendor/bin/soushi gh-pages 34 | ``` 35 | 36 | Needless to say, you need to create remote repository on GitHub and track it from your local repository in advance. 37 | 38 | ### Dynamic Web Site 39 | 40 | Upload the whole directories (**NOT** just `public/` directory) in which your site is to a Web server via FTP or anything. If the document root of Web server is not set to `public/`, rename `public/` directory to appropriate one before deployment. 41 | 42 | For Apache httpd, Soushi generates .htaccess file in `public/` directory to handle requests routing to appropriate files. 43 | 44 | ``` 45 | RewriteEngine On 46 | RewriteBase / 47 | RewriteRule ^index\.php$ - [L] 48 | RewriteCond %{REQUEST_FILENAME} !-f 49 | RewriteCond %{REQUEST_FILENAME} !-d 50 | RewriteRule . /index.php [L] 51 | ``` 52 | -------------------------------------------------------------------------------- /source/first.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Your First Page" 3 | template: page 4 | --- 5 | 6 | ### Page 7 | 8 | By default, pages are supposed to be placed in `source/` directory. Soushi supports Markdown for page contents and [Front Matter](https://jekyllrb.com/docs/frontmatter/) for metadata of a page. 9 | 10 | **source/index.md**: 11 | 12 | ```markdown 13 | --- 14 | title: My Homepage 15 | template: page 16 | --- 17 | 18 | This is my homepage! 19 | ``` 20 | 21 | The metadata properties used in above page are to: 22 | 23 | * `title`: refer to the page title 24 | * `template`: refer to the template file for the page 25 | 26 | ### Template 27 | 28 | By default, templates are supposed to be placed in `templates/` directory. Templates are just plain PHP scripts enhanced by [Plates](http://platesphp.com/) library. 29 | 30 | **templates/page.php**: 31 | 32 | ```php 33 | 34 | 35 | 36 | <?= $config->site_title() ?> 37 | 38 | 39 |

site_title() ?>

40 |

41 | 42 | 43 | 44 | ``` 45 | 46 | For details, see [Variables](./variables) and [Template](./template) sections of this site. 47 | 48 | ### Assets 49 | 50 | Files in `source/` directory that have non-`.md` extension are recognized as assets such as CSS, JavaScript, or images. You can put whatever static files you like in the directory. 51 | 52 | **source/main.css**: 53 | 54 | ```css 55 | /* some css descriptions below */ 56 | ``` 57 | 58 | **CAVEAT**: Assets are **NOT** automatically copied into `public/` directory which can be shown via preview server described below. Thus, you have to make symlinks to each assets from `public/` directory: 59 | 60 | ```sh 61 | $ cd public 62 | $ ln -sf ../source/main.css main.css 63 | ``` 64 | 65 | ### Preview 66 | 67 | Run `soushi server` to preview your site in browser. 68 | 69 | ```sh 70 | $ ./vendor/bin/soushi server 71 | ``` 72 | 73 | By default, the server launches at `http://127.0.0.1:8000`. 74 | -------------------------------------------------------------------------------- /source/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | template: page 4 | --- 5 | 6 | Soushi (草紙 in Japanese) is a bimodal site generator powered by PHP. 7 | 8 | ### Concept 9 | 10 | In what way is Soushi bimodal? 11 | 12 | Soushi is bimodal in terms of that it supports both generating a static Web site for [GitHub Pages](https://pages.github.com/) and serving contents dynamically via PHP scripts which can be simply put onto Web server, making HTML files from source files written in Markdown. 13 | 14 | Soushi exploits the strength of PHP which allows us to easily deploy scripts and which is, itself, a template language, being enhanced by [Plates](http://platesphp.com/) library. 15 | 16 | ### Source 17 | 18 | Repository: https://github.com/kentaro/soushi 19 | 20 | This Web site itself is built with Soushi. You can also consult the repository above to get an example of how to create a Web site with Soushi. 21 | 22 | ### Installation 23 | 24 | Firstly, create a directory for your awesome site and track a remote repository from there. 25 | 26 | ```sh 27 | $ mkdir your-awesome-site 28 | $ cd your-awesome-site 29 | $ git init 30 | $ git remote add origin git@github.com:your-github-account/your-awesome-site.git 31 | ``` 32 | 33 | Secondly, add dependency on kentaro/soushi using composer. 34 | 35 | ```sh 36 | $ composer require kentaro/soushi 37 | ``` 38 | 39 | And finally, initialise the project using `soushi` command. 40 | 41 | ```sh 42 | $ ./vendor/bin/soushi init 43 | ``` 44 | 45 | As a result, you can see some files and directories tree like below: 46 | 47 | ``` 48 | .gitignore 49 | config.php 50 | build/ 51 | public/ 52 | └─ .htaccess 53 | └─ index.php 54 | source/ 55 | templates/ 56 | ``` 57 | 58 | Now you are ready to enjoy Soushi! 59 | 60 | -------------------------------------------------------------------------------- /source/main.css: -------------------------------------------------------------------------------- 1 | @import url('//fonts.googleapis.com/css?family=Varela+Round'); 2 | @import url('//fonts.googleapis.com/css?family=Inconsolata'); 3 | 4 | html, body { 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | body { 11 | font-family: 'Varela Round', sans-serif; 12 | background-color: #fff; 13 | } 14 | 15 | .container { 16 | display: flex; 17 | flex-direction: column; 18 | min-height: 100vh; 19 | } 20 | 21 | .header { 22 | color: #fff; 23 | border-bottom: 1px solid #333; 24 | background-color: #006699; 25 | padding: 0 3em; 26 | } 27 | 28 | .header a:link, 29 | .header a:visited { 30 | color: #fff; 31 | text-decoration: none; 32 | } 33 | 34 | .body { 35 | display: flex; 36 | flex-direction: row; 37 | min-height: 100vh; 38 | } 39 | 40 | .sidebar { 41 | color: #fff; 42 | border-right: 1px solid #333; 43 | background-color: #0099CC; 44 | flex-direction: column; 45 | padding: 1em; 46 | width: 15%; 47 | } 48 | 49 | .sidebar ul { 50 | list-style: none; 51 | margin-left: 0; 52 | padding-left: 0; 53 | } 54 | 55 | .sidebar a:link, 56 | .sidebar a:visited { 57 | color: #f0f0f0; 58 | } 59 | 60 | .main { 61 | padding: 0 3em; 62 | flex: 1; 63 | } 64 | 65 | .main h2, 66 | .main h3, 67 | .main h4, 68 | .main h5, 69 | .main h6 { 70 | color: #003366; 71 | font-weight: bold; 72 | } 73 | 74 | .main a:link { 75 | color: #0099CC; 76 | } 77 | 78 | .main a:visited { 79 | color: #006699; 80 | } 81 | 82 | .footer { 83 | color: #fff; 84 | background: #333; 85 | text-align: center; 86 | padding: 1em; 87 | border-top: 1px solid #333; 88 | } 89 | 90 | .footer a:link, 91 | .footer a:visited { 92 | color: #f0f0f0; 93 | } 94 | 95 | pre { 96 | line-height: 1.4em; 97 | font-family: 'Inconsolata', monospace; 98 | border-radius: 10px; 99 | background-color: #f0f0f0; 100 | padding: 1em; 101 | } 102 | 103 | code { 104 | font-family: 'Inconsolata', monospace; 105 | background-color: #f0f0f0; 106 | padding: 0.2em; 107 | } 108 | -------------------------------------------------------------------------------- /source/template.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Template 3 | template: page 4 | --- 5 | 6 | ### PHP as a Template Engine 7 | 8 | Templates must be placed in `templates/` directory (or some other place you set in your `config.php`). 9 | 10 | They are just plain PHP script enhanced by [Plates](http://platesphp.com/) library. Thus, you can whatever you can do with PHP. 11 | 12 | ### Layouts 13 | 14 | If you build a certain level of large Web site, you will want to divide templates to multiple files. Layout feature is helpful for such a situation. 15 | 16 | Soushi's template system just exploits PHP and Plates library mentioned above. Thus you can build your site with layouts using a feature of Plates. 17 | 18 | See [Layouts - Plates](http://platesphp.com/templates/layouts/) for details. 19 | -------------------------------------------------------------------------------- /source/variables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Variables 3 | template: page 4 | --- 5 | 6 | ### Configuration 7 | 8 | * `$config` 9 | 10 | Refers to the configuration in `config.php`. See [Configuration](./config) section for details. 11 | 12 | ### Page Matadata 13 | 14 | * `$title` 15 | * `$template` 16 | 17 | Metadata variables come from the metadata section of page written in Front Matter like below. 18 | 19 | ``` 20 | --- 21 | title: Variables 22 | template: page 23 | --- 24 | ``` 25 | 26 | ### Page Content 27 | 28 | * `$content` 29 | 30 | Contains an HTML string generated from Markdown-formatted string in a page. 31 | -------------------------------------------------------------------------------- /src/Aggregator.php: -------------------------------------------------------------------------------- 1 | sourceDir = $sourceDir; 18 | } 19 | 20 | function files(): array 21 | { 22 | if (is_null($this->files)) { 23 | $this->files = []; 24 | foreach($this->iterator() as $file) { 25 | $this->files[] = File\Factory::create($file); 26 | } 27 | } 28 | 29 | return $this->files; 30 | } 31 | 32 | function pages(): array 33 | { 34 | if (is_null($this->pages)) { 35 | $this->pages = array_filter($this->files(), function ($e) { 36 | return $e->isPage(); 37 | }); 38 | } 39 | 40 | return $this->pages; 41 | } 42 | 43 | function assets(): array 44 | { 45 | if (is_null($this->assets)) { 46 | $this->assets = array_filter($this->files(), function ($e) { 47 | return !$e->isPage(); 48 | }); 49 | } 50 | 51 | return $this->assets; 52 | } 53 | 54 | function fetch(string $path): File 55 | { 56 | if ($path == "") { 57 | $path = "index"; 58 | } 59 | 60 | foreach($this->files() as $file) { 61 | if ($file->isPage() && ($file->path() == $path)) { 62 | return $file; 63 | } 64 | } 65 | 66 | throw new \Soushi\Exception\PageNotFound("page not found"); 67 | } 68 | 69 | private function iterator(): \Symfony\Component\Finder\Finder 70 | { 71 | if (is_null($this->finder)) { 72 | $this->finder = new \Symfony\Component\Finder\Finder(); 73 | $this->finder->files()->in($this->sourceDir); 74 | } 75 | 76 | return $this->finder; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Cli.php: -------------------------------------------------------------------------------- 1 | 1) { 13 | $subcommand = $argv[1]; 14 | $args = array_slice($argv, 2); 15 | } 16 | 17 | switch ($subcommand) { 18 | case 'init': 19 | $this->init($args); 20 | break; 21 | case 'server': 22 | $this->server($args); 23 | break; 24 | case 'build': 25 | $this->build($args); 26 | break; 27 | case 'gh-pages': 28 | $this->ghPages($args); 29 | break; 30 | default: 31 | $this->usage($argv[0]); 32 | break; 33 | } 34 | } 35 | 36 | private function usage(string $command) 37 | { 38 | echo "usage: {$command} [subcommand]\n"; 39 | exit(1); 40 | } 41 | 42 | private function init(array $args) 43 | { 44 | $command = new \Soushi\Command\Init($args[0] ?? "."); 45 | $command->execute(); 46 | } 47 | 48 | private function server(array $args) 49 | { 50 | $hostport = $args[0] ?? "127.0.0.1:8000"; 51 | $docroot = $args[1] ?? "public"; 52 | 53 | pcntl_exec("/usr/bin/env", ["php", "-S", $hostport, "-t", $docroot]); 54 | } 55 | 56 | private function build(array $args) 57 | { 58 | $command = new \Soushi\Command\Build($args[0] ?? "build"); 59 | $command->execute(); 60 | } 61 | 62 | private function ghPages(array $args) 63 | { 64 | $command = new \Soushi\Command\Ghpages($args[0] ?? "build"); 65 | $command->execute(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | dstDir = $dstDir; 19 | } 20 | 21 | private function filePutContentsDeeply($path, $contents) 22 | { 23 | $parts = explode("/", $path); 24 | $file = array_pop($parts); 25 | $dir = ""; 26 | 27 | if (count($parts) > 0) { 28 | foreach ($parts as $part) { 29 | if (!is_dir($dir .= "$part/")) { 30 | mkdir($dir); 31 | } 32 | } 33 | 34 | } 35 | 36 | return file_put_contents("$dir/$file", $contents); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Command/Build.php: -------------------------------------------------------------------------------- 1 | prepareDirectory($dstDir); 16 | 17 | $this->config = \Soushi\Config::loadFile("config.php"); 18 | $this->template = new \Soushi\Template($this->config->template_dir()); 19 | $this->aggregator = new \Soushi\Aggregator($this->config->source_dir()); 20 | } 21 | 22 | function execute() 23 | { 24 | $this->generatePages(); 25 | $this->generateAssets(); 26 | } 27 | 28 | private function generatePages() 29 | { 30 | foreach ($this->aggregator->pages() as $page) { 31 | $path = "{$this->dstDir}/{$page->path()}.html"; 32 | $html = $this->template->render( 33 | $page->template(), 34 | array_merge( 35 | $page->metadata(), 36 | [ 37 | "config" => $this->config, 38 | "pages" => $this->aggregator->pages(), 39 | "page" => $page, 40 | "content" => $page->content(), 41 | ] 42 | ) 43 | ); 44 | $this->filePutContentsDeeply($path, $html); 45 | } 46 | } 47 | 48 | private function generateAssets() 49 | { 50 | foreach ($this->aggregator->assets() as $asset) { 51 | $path = "{$this->dstDir}/{$asset->pathWithExtension()}"; 52 | $contents = file_get_contents($asset->absolutePath()); 53 | $this->filePutContentsDeeply($path, $contents); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Command/Ghpages.php: -------------------------------------------------------------------------------- 1 | prepareDirectory($dstDir); 12 | } 13 | 14 | // ref. http://motemen.hatenablog.com/entry/2014/01/24/GitHub_pages_%E3%81%AB_push_%E3%81%99%E3%82%8B%E3%82%B7%E3%82%A7%E3%83%AB%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88 15 | function execute() 16 | { 17 | $remote = system("git config remote.origin.url"); 18 | $rev = system("git rev-parse HEAD | git name-rev --stdin"); 19 | 20 | chdir($this->dstDir); 21 | $cdup = system("git rev-parse --show-cdup"); 22 | if (!empty($cdup)) { 23 | system("git init"); 24 | system("git remote add --fetch origin {$remote}"); 25 | } 26 | 27 | if (system("git rev-parse --verify origin/gh-pages > /dev/null 2>&1")) { 28 | system("git checkout gh-pages"); 29 | } else { 30 | system("git checkout --orphan gh-pages"); 31 | } 32 | 33 | system("git add ."); 34 | system("git commit -m 'pages built at {$rev} -e'"); 35 | system("git push origin gh-pages"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Command/Init.php: -------------------------------------------------------------------------------- 1 | prepareDirectory($dstDir); 12 | } 13 | 14 | function execute() 15 | { 16 | $this->generateGitIgnore(); 17 | $this->generateDirectories(); 18 | $this->generateConfigPhp(); 19 | $this->generateIndexPhp(); 20 | $this->generateDotHtaccess(); 21 | } 22 | 23 | private function generateGitIgnore() 24 | { 25 | $content = <<<'EOS' 26 | /vendor 27 | composer.lock 28 | 29 | /build 30 | /public 31 | /tmp 32 | EOS; 33 | if (!file_put_contents("{$this->dstDir}/.gitignore", $content)) { 34 | throw new \Soushi\Exception\File("failed to create .gitignore"); 35 | } 36 | } 37 | 38 | private function generateDirectories() 39 | { 40 | $dirs = ["public", "source", "templates", "build"]; 41 | foreach ($dirs as $dir) { 42 | $path = $this->dstDir . "/{$dir}"; 43 | if (!file_exists($path) && !mkdir($path, 0755)) { 44 | throw new \Soushi\Exception\File("failed to create {$dir}"); 45 | } 46 | } 47 | } 48 | 49 | private function generateConfigPhp() 50 | { 51 | $content = <<<'EOS' 52 | "My Homepage", 55 | "template_dir" => dirname(__FILE__) . "/templates", 56 | "source_dir" => dirname(__FILE__) . "/source", 57 | ]; 58 | EOS; 59 | $path = "{$this->dstDir}/config.php"; 60 | if ( 61 | !file_exists($path) && 62 | !file_put_contents($path, $content) 63 | ) { 64 | throw new \Soushi\Exception\File("failed to create config.php"); 65 | } 66 | } 67 | 68 | private function generateIndexPhp() 69 | { 70 | $content = <<<'EOS' 71 | dispatch($_SERVER["REQUEST_URI"]); 78 | EOS; 79 | $path = "{$this->dstDir}/public/index.php"; 80 | if ( 81 | !file_exists($path) && 82 | !file_put_contents($path, $content) 83 | ) { 84 | throw new \Soushi\Exception\File("failed to create index.php"); 85 | } 86 | } 87 | 88 | private function generateDotHtaccess() 89 | { 90 | $content = <<<'EOS' 91 | RewriteEngine On 92 | RewriteBase / 93 | RewriteRule ^index\.php$ - [L] 94 | RewriteCond %{REQUEST_FILENAME} !-f 95 | RewriteCond %{REQUEST_FILENAME} !-d 96 | RewriteRule . /index.php [L] 97 | EOS; 98 | $path = "{$this->dstDir}/public/.htaccess"; 99 | if ( 100 | !file_exists($path) && 101 | !file_put_contents($path, $content) 102 | ) { 103 | throw new \Soushi\Exception\File("failed to create .htaccess"); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | build($config); 18 | } 19 | 20 | function __call($name, $args = []) 21 | { 22 | return $this->config[$name]; 23 | } 24 | 25 | private function build(array $config) 26 | { 27 | $config["template_dir"] = $config["template_dir"] ?? 28 | dirname(__FILE__, 2) . "/templates"; 29 | $config["source_dir"] = $config["source_dir"] ?? 30 | dirname(__FILE__, 2) . "/source"; 31 | 32 | $this->config = $config; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Exception/File.php: -------------------------------------------------------------------------------- 1 | file = $file; 15 | } 16 | 17 | function isPage(): bool 18 | { 19 | return false; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/File/Base.php: -------------------------------------------------------------------------------- 1 | file->getExtension()}$/", '$1', $this->file->getRelativePathname()); 10 | } 11 | 12 | function pathWithExtension(): string 13 | { 14 | return $this->file->getRelativePathname(); 15 | } 16 | 17 | function absolutePath(): string 18 | { 19 | return $this->file->getRealPath(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/File/Factory.php: -------------------------------------------------------------------------------- 1 | getPathname())) { 12 | return new Page($file); 13 | } else { 14 | return new Asset($file); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/File/Page.php: -------------------------------------------------------------------------------- 1 | file = $file; 15 | } 16 | 17 | function isPage(): bool 18 | { 19 | return true; 20 | } 21 | 22 | function metadata(): array 23 | { 24 | return $this->document()->getYAML(); 25 | } 26 | 27 | function content(): string 28 | { 29 | return $this->document()->getContent(); 30 | } 31 | 32 | function template(): string 33 | { 34 | return $this->metadata()["template"]; 35 | } 36 | 37 | private function document(): \Mni\FrontYAML\Document 38 | { 39 | if(is_null($this->document)) { 40 | $this->document = \Soushi\Parser::getInstance()->parse(file_get_contents($this->file)); 41 | } 42 | 43 | return $this->document; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | parser = new \Mni\FrontYAML\Parser(); 22 | } 23 | 24 | function parse($content): \Mni\FrontYAML\Document 25 | { 26 | return $this->parser->parse($content); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Template.php: -------------------------------------------------------------------------------- 1 | dirname = $dirname; 13 | } 14 | 15 | function engine(): \League\Plates\Engine 16 | { 17 | if (is_null($this->engine)) { 18 | $this->engine = new \League\Plates\Engine($this->dirname); 19 | } 20 | 21 | return $this->engine; 22 | } 23 | 24 | function render(string $name, array $params = []) 25 | { 26 | return $this->engine()->render($name, $params); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Web.php: -------------------------------------------------------------------------------- 1 | config = $config; 14 | $this->template = new Template($config->template_dir()); 15 | $this->aggregator = new Aggregator($config->source_dir()); 16 | } 17 | 18 | function dispatch(string $path): string 19 | { 20 | try { 21 | $page = $this->aggregator->fetch($this->normalizePath($path)); 22 | } catch (Exception\PageNotFound $e) { 23 | return $this->handle404($e); 24 | } catch (Throwable $e) { 25 | return $this->handle500($e); 26 | } 27 | 28 | try { 29 | $html = $this->template->render( 30 | $page->template(), 31 | array_merge( 32 | $page->metadata(), 33 | [ 34 | "config" => $this->config, 35 | "pages" => $this->aggregator->pages(), 36 | "page" => $page, 37 | "content" => $page->content(), 38 | ] 39 | ) 40 | ); 41 | } catch (Throwable $e) { 42 | return $this->handle500($e); 43 | } 44 | 45 | return $html; 46 | } 47 | 48 | function normalizePath(string $path): string 49 | { 50 | return preg_replace("/^\/(index\.php\/?)?/", "", $path); 51 | } 52 | 53 | private function handle404(Exception\PageNotFound $e) 54 | { 55 | http_response_code(404); 56 | return $e->getMessage(); 57 | } 58 | 59 | private function handle500(Throwable $e) 60 | { 61 | http_response_code(500); 62 | return $e->getMessage(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /templates/page.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <?= $config->site_title() ?> 9 | 10 | 11 |
12 |
13 |

site_title() ?>

14 |
15 |
16 | 34 |
35 |

36 | 37 |
38 |
39 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/AggregatorTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Soushi\Aggregator::class, $aggregator); 11 | } 12 | 13 | function testFiles() 14 | { 15 | $aggregator = new Soushi\Aggregator(dirname(__FILE__) . "/assets/source"); 16 | $files = $aggregator->files(); 17 | $this->assertEquals(count($files), 5); 18 | } 19 | 20 | function testPages() 21 | { 22 | $aggregator = new Soushi\Aggregator(dirname(__FILE__) . "/assets/source"); 23 | $files = $aggregator->pages(); 24 | $this->assertEquals(count($files), 3); 25 | } 26 | 27 | function testAssets() 28 | { 29 | $aggregator = new Soushi\Aggregator(dirname(__FILE__) . "/assets/source"); 30 | $files = $aggregator->assets(); 31 | $this->assertEquals(count($files), 2); 32 | } 33 | 34 | function testFetch() 35 | { 36 | $aggregator = new Soushi\Aggregator(dirname(__FILE__) . "/assets/source"); 37 | $file = $aggregator->fetch("subdir/foo"); 38 | $this->assertEquals($file->path(), "subdir/foo"); 39 | } 40 | 41 | /** 42 | * @expectedException \Soushi\Exception\PageNotFound 43 | */ 44 | function testFetchFailure() 45 | { 46 | $aggregator = new Soushi\Aggregator(dirname(__FILE__) . "/assets/source"); 47 | $file = $aggregator->fetch("no such page"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Command/BuildTest.php: -------------------------------------------------------------------------------- 1 | execute(); 31 | 32 | $this->assertFileExists(self::$buildDir . "/index.html"); 33 | $this->assertFileExists(self::$buildDir . "/subdir/foo.html"); 34 | $this->assertFileExists(self::$buildDir . "/subdir/bar.html"); 35 | $this->assertFileExists(self::$buildDir . "/css/main.css"); 36 | $this->assertFileExists(self::$buildDir . "/js/main.js"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Command/GhpagesTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Soushi\Command\Ghpages::class, $ghpages); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Command/InitTest.php: -------------------------------------------------------------------------------- 1 | execute(); 25 | 26 | $this->assertFileExists(self::$tmpDir . "/.gitignore"); 27 | $this->assertFileExists(self::$tmpDir . "/public"); 28 | $this->assertFileExists(self::$tmpDir . "/source"); 29 | $this->assertFileExists(self::$tmpDir . "/templates"); 30 | $this->assertFileExists(self::$tmpDir . "/build"); 31 | $this->assertFileExists(self::$tmpDir . "/config.php"); 32 | $this->assertFileExists(self::$tmpDir . "/public/index.php"); 33 | $this->assertFileExists(self::$tmpDir . "/public/.htaccess"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | "My Homepage", 11 | "template_dir" => "/path/to/templates", 12 | "source_dir" => "/path/to/source", 13 | ]); 14 | $this->assertInstanceOf(Soushi\Config::class, $config); 15 | } 16 | 17 | function testBuild() 18 | { 19 | $config = new Soushi\Config([ 20 | "site_title" => "My Homepage", 21 | ]); 22 | 23 | $this->assertEquals($config->template_dir(), dirname(__FILE__, 2) . "/templates"); 24 | $this->assertEquals($config->source_dir(), dirname(__FILE__, 2) . "/source"); 25 | $this->assertEquals($config->site_title(), "My Homepage"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/File/AssetTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Soushi\File\Asset::class, $asset); 11 | } 12 | 13 | function testIsPage() 14 | { 15 | $asset = new Soushi\File\Asset(new \SplFileInfo(dirname(__FILE__, 2) . "/assets/page.md")); 16 | $this->assertFalse($asset->isPage()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/File/FactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Soushi\File\Page::class, $page); 11 | 12 | $asset = Soushi\File\Factory::create(new \SplFileInfo("test.css")); 13 | $this->assertInstanceOf(Soushi\File\Asset::class, $asset); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/File/PageTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Soushi\File\Page::class, $page); 11 | } 12 | 13 | function testIsEntry() 14 | { 15 | $page = new Soushi\File\Page(new \SplFileInfo(dirname(__FILE__, 2) . "/assets/source/index.md")); 16 | $this->assertTrue($page->isPage()); 17 | } 18 | 19 | function testMetadata() 20 | { 21 | $page = new Soushi\File\Page(new \SplFileInfo(dirname(__FILE__, 2) . "/assets/source/index.md")); 22 | $this->assertEquals($page->metadata(), [ 23 | "title" => "test site", 24 | "author" => "kentaro", 25 | "template" => "index", 26 | ]); 27 | } 28 | 29 | function testTemplate() 30 | { 31 | $page = new Soushi\File\Page(new \SplFileInfo(dirname(__FILE__, 2) . "/assets/source/index.md")); 32 | $this->assertEquals($page->template(), "index"); 33 | } 34 | 35 | function testContent() 36 | { 37 | $page = new Soushi\File\Page(new \SplFileInfo(dirname(__FILE__, 2) . "/assets/source/index.md")); 38 | $this->assertEquals($page->content(), <<Hello, World.

40 | EOS 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/ParserTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 10 | Soushi\Parser::getInstance(), 11 | Soushi\Parser::getInstance() 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/TemplateTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Soushi\Template::class, $tmpl); 11 | } 12 | 13 | function testRender() 14 | { 15 | $tmpl = new Soushi\Template(dirname(__FILE__) . "/assets/templates"); 16 | $this->assertEquals( 17 | $tmpl->render("index", [ 18 | "title" => "test site", 19 | "content" => "page list", 20 | ]), 21 | <<test site 23 | 24 |

Index

25 | 26 | page list 27 | EOS 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/WebTest.php: -------------------------------------------------------------------------------- 1 | dirname(__FILE__) . "/assets/templates", 11 | "source_dir" => dirname(__FILE__) . "/assets/source", 12 | ]); 13 | $web = new Soushi\Web($config); 14 | $this->assertInstanceOf(Soushi\Web::class, $web); 15 | } 16 | 17 | function testDispatch() 18 | { 19 | $config = new Soushi\Config([ 20 | "template_dir" => dirname(__FILE__) . "/assets/templates", 21 | "source_dir" => dirname(__FILE__) . "/assets/source", 22 | ]); 23 | $web = new Soushi\Web($config); 24 | $html = $web->dispatch("/subdir/foo"); 25 | 26 | $this->assertStringStartsWith("<", $html); 27 | } 28 | 29 | function testNormalizePath() 30 | { 31 | $config = new Soushi\Config([ 32 | "template_dir" => dirname(__FILE__) . "/assets/templates", 33 | "source_dir" => dirname(__FILE__) . "/assets/source", 34 | ]); 35 | $web = new Soushi\Web($config); 36 | 37 | $path = $web->normalizePath("/"); 38 | $this->assertSame("", $path); 39 | 40 | $path = $web->normalizePath("/index.php"); 41 | $this->assertSame("", $path); 42 | 43 | $path = $web->normalizePath("/foo/bar"); 44 | $this->assertSame("foo/bar", $path); 45 | 46 | $path = $web->normalizePath("/index.php/foo/bar"); 47 | $this->assertSame("foo/bar", $path); 48 | } 49 | 50 | function testHandle404() 51 | { 52 | $config = new Soushi\Config([ 53 | "template_dir" => dirname(__FILE__) . "/assets/templates", 54 | "source_dir" => dirname(__FILE__) . "/assets/source", 55 | ]); 56 | $web = new Soushi\Web($config); 57 | $html = $web->dispatch("/no/such/path"); 58 | 59 | $this->assertSame("page not found", $html); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/assets/config.php: -------------------------------------------------------------------------------- 1 | "My Homepage", 4 | "template_dir" => dirname(__FILE__) . "/templates", 5 | "source_dir" => dirname(__FILE__) . "/source", 6 | ]; 7 | -------------------------------------------------------------------------------- /tests/assets/source/css/main.css: -------------------------------------------------------------------------------- 1 | /* css file for test */ 2 | -------------------------------------------------------------------------------- /tests/assets/source/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: test site 3 | author: kentaro 4 | template: index 5 | --- 6 | 7 | Hello, **World**. 8 | -------------------------------------------------------------------------------- /tests/assets/source/js/main.js: -------------------------------------------------------------------------------- 1 | // js file for test 2 | -------------------------------------------------------------------------------- /tests/assets/source/subdir/bar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: subdir/bar 3 | author: kentaro 4 | template: page 5 | --- 6 | 7 | subdir bar 8 | -------------------------------------------------------------------------------- /tests/assets/source/subdir/foo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: subdir/foo 3 | author: kentaro 4 | template: page 5 | --- 6 | 7 | subdir foo 8 | -------------------------------------------------------------------------------- /tests/assets/templates/index.php: -------------------------------------------------------------------------------- 1 | layout("layout", ["title" => $title]) 3 | ?> 4 | 5 |

Index

6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/assets/templates/layout.php: -------------------------------------------------------------------------------- 1 | <?= $title ?> 2 | section("content") ?> 3 | -------------------------------------------------------------------------------- /tests/assets/templates/page.php: -------------------------------------------------------------------------------- 1 | layout("layout", ["title" => $title]) 3 | ?> 4 | 5 |

6 | 7 | 8 | --------------------------------------------------------------------------------