├── .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 [](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 | = $config->site_title() ?>
40 | = $title ?>
41 | = $content ?>
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 |
15 |
16 |
34 |
35 |
= $title ?>
36 | = $content ?>
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 | = $content ?>
8 |
--------------------------------------------------------------------------------
/tests/assets/templates/layout.php:
--------------------------------------------------------------------------------
1 | = $title ?>
2 | = $this->section("content") ?>
3 |
--------------------------------------------------------------------------------
/tests/assets/templates/page.php:
--------------------------------------------------------------------------------
1 | layout("layout", ["title" => $title])
3 | ?>
4 |
5 | = $title ?>
6 |
7 | = $content ?>
8 |
--------------------------------------------------------------------------------