Explorar o código

Merge pull request #252 from PhrozenByte/pico1.0

Pico 1.0
Daniel Rudolf %!s(int64=9) %!d(string=hai) anos
pai
achega
e5b0ec6d7b

+ 6 - 1
.htaccess

@@ -4,7 +4,12 @@
     #RewriteBase /
     #RewriteBase /
     RewriteCond %{REQUEST_FILENAME} !-f
     RewriteCond %{REQUEST_FILENAME} !-f
     RewriteCond %{REQUEST_FILENAME} !-d
     RewriteCond %{REQUEST_FILENAME} !-d
-    RewriteRule . index.php [L]
+    RewriteRule ^(.*)$ index.php?$1 [L,QSA]
+    RewriteRule ^(config|content|content-sample|lib|vendor)/.* - [R=404,L]
+
+    <IfModule mod_env.c>
+        SetEnv PICO_URL_REWRITING 1
+    </IfModule>
 </IfModule>
 </IfModule>
 
 
 # Prevent file browsing
 # Prevent file browsing

+ 29 - 0
.travis.yml

@@ -0,0 +1,29 @@
+language: php
+php:
+  - 5.3
+  - 5.4
+  - 5.5
+  - 5.6
+  - 7
+  - hhvm
+  - nightly
+
+script:
+  - find . -type f -name '*.php' -print0 | xargs -0 -I file php -l file > /dev/null
+
+before_deploy:
+  - composer install
+  - tar -czf "pico-release-$TRAVIS_TAG.tar.gz" .htaccess README.md changelog.txt composer.json composer.lock license.txt config content-sample lib plugins themes vendor index.php
+
+deploy:
+  provider: releases
+  api_key: ${GITHUB_OAUTH_TOKEN}
+  file: pico-release-$TRAVIS_TAG.tar.gz
+  skip_cleanup: true
+  on:
+    repo: picocms/Pico
+    tags: true
+    php: 5.3
+
+sudo: false
+

+ 197 - 0
CHANGELOG.md

@@ -0,0 +1,197 @@
+Pico Changelog
+==============
+
+### Version 1.0.0-beta.1
+Released: 2015-11-06
+
+**Note:** This changelog only provides basic information about the enormous
+          changes introduced with Pico 1.0.0-beta.1. Please refer to the
+          UGPRADE section of the docs for details.
+
+```
+* [Security] (9e2604a) Prevent content_dir breakouts using malicious URLs
+* [New] Pico is on its way to its first stable release!
+* [New] Provide pre-bundled releases
+* [New] Heavily expanded documentation (inline code docs, user docs, dev docs)
+* [New] New routing system using the QUERY_STRING method; Pico now works
+        out-of-the-box with any webserver and without URL rewriting; use
+        `%base_url%?sub/page` in markdown files and `{{ "sub/page"|link }}`
+        in Twig templates to declare internal links
+* [New] Brand new plugin system with dependencies (see `PicoPluginInterface`
+        and `AbstractPicoPlugin`); if you're plugin dev, you really should
+        take a look at the UPGRADE section of the docs!
+* [New] Introducing the `PicoDeprecated` plugin to maintain full backward
+        compatibility with Pico 0.9 and Pico 0.8
+* [New] Support YAML-style meta header comments (`---`)
+* [New] Various new placeholders to use in content files (e.g. `%site_title%`)
+* [New] Provide access to all meta headers in content files (`%meta.*%`)
+* [New] Provide access to meta headers in `$page` arrays (`$page['meta']`)
+* [New] The file extension of content files is now configurable
+* [New] Add `Pico::setConfig()` method to predefine config variables
+* [New] Supporting per-directory `404.md` files
+* [New] #103: Providing access to `sub.md` even when the `sub` directory
+        exists, provided that there is no `sub/index.md`
+* [New] #249: Support the `.twig` file extension for templates
+* [New] #268, 269: Now using Travis CI; performing basic code tests and
+        implementing an automatic release process
+* [Changed] Complete code refactoring
+* [Changed] Source code now follows PSR code styling
+* [Changed] Replacing constants (e.g. `ROOT_DIR`) with constructor parameters
+* [Changed] Paths (e.g. `content_dir`) are now relative to Pico's root dir
+* [Changed] Adding `Pico::run()` method that performs Pico's processing and
+            returns the rendered contents
+* [Changed] Renaming all plugin events; adding some new events
+* [Changed] `Pico_Plugin` is now the fully documented `DummyPlugin`
+* [Changed] Meta data must start on the first line of the file now
+* [Changed] Dropping the need to register meta headers for the convenience of
+            users and pure (!) theme devs; plugin devs are still REQUIRED to
+            register their meta headers during `onMetaHeaders`
+* [Changed] Exclude inaccessible files from pages list
+* [Changed] With alphabetical order, index files (e.g. `sub/index.md`) are
+            now always placed before their sub pages (e.g. `sub/foo.md`)
+* [Changed] Pico requires PHP >= 5.3.6 (due to `erusev/parsedown-extra`)
+* [Changed] Pico now implicitly uses a existing `content` directory without
+            the need to configure this in the `config/config.php` explicitly
+* [Changed] Composer: Require a v0.7 release of `erusev/parsedown-extra`
+* [Changed] #93, #158: Pico doesn't parse all content files anymore; moved to
+            `PicoParsePagesContent` plugin, but still impacts performance;
+            Note: This means `$page['content']` isn't available anymore, but
+            usually the new `$page['raw_content']` is suitable as replacement.
+* [Changed] #116: Parse meta headers using the Symfony YAML component
+* [Changed] #244: Replace opendir() with scandir()
+* [Changed] #246: Move `config.php` to `config/` directory
+* [Changed] #253: Assume HTTPS if page is requested through port 443
+* [Changed] A vast number of small improvements and changes...
+* [Fixed] Sorting by date now uses timestamps and works as expected
+* [Fixed] Fixing `$currentPage`, `$nextPage` and `$previousPage`
+* [Fixed] #99: Support content filenames with spaces
+* [Fixed] #140, #241: Use file paths as page identifiers rather than titles
+* [Fixed] #248: Always set a timezone; adding `$config['timezone']` option
+* [Fixed] A vast number of small bugs...
+* [Removed] Removing the default Twig cache dir
+* [Removed] Removing various empty `index.html` files
+* [Removed] Moving Pico's excerpt feature to `PicoExcerpt` plugin
+```
+
+### Version 0.9
+Released: 2015-04-28
+
+```
+* [New] Default theme is now mobile-friendly
+* [New] Description meta now available in content areas
+* [New] Add description to composer.json
+* [Changed] content folder is now content-sample
+* [Changed] config.php moved to config.php.template
+* [Changed] Updated documentation & wiki
+* [Changed] Removed Composer, Twig files in /vendor, you must run composer
+            install now
+* [Changed] Localized date format; strftime() instead of date()
+* [Changed] Added ignore for tmp file extensions in the get_files() method
+* [Changed] michelf/php-markdown is replaced with erusev/parsedown-extra
+* [Changed] $config is no global variable anymore
+* [Fixed] Pico now only removes the 1st comment block in .md files
+* [Fixed] Issue wherein the alphabetical sorting of pages did not happen
+```
+
+### Version 0.8
+Released: 2013-10-23
+
+```
+* [New] Added ability to set template in content meta
+* [New] Added before_parse_content and after_parse_content hooks
+* [Changed] content_parsed hook is now deprecated
+* [Changed] Moved loading the config to nearer the beginning of the class
+* [Changed] Only append ellipsis in limit_words() when word count exceeds max
+* [Changed] Made private methods protected for better inheritance
+* [Fixed] Fixed get_protocol() method to work in more situations
+```
+
+### Version 0.7
+Released: 2013-09-04
+
+```
+* [New] Added before_read_file_meta and get_page_data plugin hooks to customize
+        page meta data
+* [Changed] Make get_files() ignore dotfiles
+* [Changed] Make get_pages() ignore Emacs and temp files
+* [Changed] Use composer version of Markdown
+* [Changed] Other small tweaks
+* [Fixed] Date warnings and other small bugs
+```
+
+### Version 0.6.2
+Released: 2013-05-07
+
+```
+* [Changed] Replaced glob_recursive with get_files
+```
+
+### Version 0.6.1
+Released: 2013-05-07
+
+```
+* [New] Added "content" and "excerpt" fields to pages
+* [New] Added excerpt_length config setting
+```
+
+### Version 0.6
+Released: 2013-05-06
+
+```
+* [New] Added plugin functionality
+* [Changed] Other small cleanup
+```
+
+### Version 0.5
+Released: 2013-05-03
+
+```
+* [New] Added ability to order pages by "alpha" or "date" (asc or desc)
+* [New] Added prev_page, current_page, next_page and is_front_page template vars
+* [New] Added "Author" and "Date" title meta fields
+* [Changed] Added "twig_config" to settings
+* [Changed] Updated documentation
+* [Fixed] Query string 404 bug
+```
+
+### Version 0.4.1
+Released: 2013-05-01
+
+```
+* [New] Added CONTENT_EXT global
+* [Changed] Use .md files instead of .txt
+```
+
+### Version 0.4
+Released: 2013-05-01
+
+```
+* [New] Add get_pages() function for listing content
+* [New] Added changelog.txt
+* [Changed] Updated default theme
+* [Changed] Updated documentation
+```
+
+### Version 0.3
+Released: 2013-04-27
+
+```
+* [Fixed] get_config() function
+```
+
+### Version 0.2
+Released: 2013-04-26
+
+```
+* [Changed] Updated Twig
+* [Changed] Better checking for HTTPS
+* [Fixed] Add 404 header to 404 page
+* [Fixed] Case sensitive folder bug
+```
+
+### Version 0.1
+Released: 2012-04-04
+
+```
+* Initial release
+```

+ 119 - 0
CONTRIBUTING.md

@@ -0,0 +1,119 @@
+Contributing to Pico
+====================
+
+Pico aims to be a high quality Content Management System (CMS) but at the same time wants to give contributors freedom when submitting fixes or improvements.
+
+As such we want to *encourage* but not obligate you, the contributor, to follow these guidelines. The only exception to this are the guidelines elucidated in the *Prevent `merge-hell`* section.
+
+Having said that: we really appreciate it when you apply the guidelines in part or wholly as that will save us time which, in turn, we can spend on bugfixes and new features.
+
+Issues
+------
+
+If you want to report an *issue* with Pico's core, please create a new [Issue](https://github.com/picocms/Pico/issues) on GitHub. Concerning problems with plugins or themes, please refer to the website of the developer of this plugin or theme.
+
+Before creating a [new Issue on GitHub](https://github.com/picocms/Pico/issues/new), please make sure the problem wasn't reported yet using [GitHubs search engine](https://github.com/picocms/Pico/search?type=Issues). Please describe your issue as clear as possible and always include steps to reproduce the problem.
+
+Contributing code
+-----------------
+
+Once you decide you want to contribute to *Pico's core* (which we really appreciate!) you can fork the project from https://github.com/picocms/Pico. If you're interested in developing a *plugin* or *theme* for Pico, please refer to the [development section](http://picocms.org/plugin-dev.html) of our website.
+
+### Prevent `merge-hell`
+
+Please do *not* develop your contribution on the `master` branch of your fork, but create a separate feature branch, that is based off the `master` branch, for each feature that you want to contribute.
+
+> Not doing so means that if you decide to work on two separate features and place a pull request for one of them, that the changes of the other issue that you are working on is also submitted. Even if it is not completely finished.
+
+To get more information about the usage of Git, please refer to the [Pro Git book](https://git-scm.com/book) written by Scott Chacon and/or [this help page of GitHub](https://help.github.com/articles/using-pull-requests).
+
+### Pull Requests
+
+Please keep in mind that pull requests should be small (i.e. one feature per request), stick to existing coding conventions and documentation should be updated if required. It's encouraged to make commits of logical units and check for unnecessary whitespace before committing (try `git diff --check`). Please reference issue numbers in your commit messages where appropriate.
+
+### Coding Standards
+
+Pico uses the [PSR-2 Coding Standard](http://www.php-fig.org/psr/psr-2/) as defined by the [PHP Framework Interoperability Group (PHP-FIG)](http://www.php-fig.org/).
+
+For historical reasons we don't use formal namespaces. Markdown files in the `content-sample` folder (the inline documentation) must follow a hard limit of 80 characters line length.
+
+It is recommended to check your code using [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) using the `PSR2` standard using the following command:
+
+    $ ./bin/phpcs --standard=PSR2 [file(s)]
+
+With this command you can specify a file or folder to limit which files it will check or omit that argument altogether, in which case the current directory is checked.
+
+### Keep documentation in sync
+
+Pico accepts the problems of having redundant documentation on different places (concretely Pico's inline user docs, the `README.md` and the website) for the sake of a better user experience. When updating the docs, please make sure the keep them in sync.
+
+If you update the [`README.md`](https://github.com/picocms/Pico/blob/master/README.md) or [`content-sample/index.md`](https://github.com/picocms/Pico/blob/master/content-sample/index.md), please make sure to update the corresponding files in the [`_docs`](https://github.com/picocms/Pico/tree/gh-pages/_docs/) folder of the `gh-pages` branch (i.e. [Pico's website](http://picocms.org/docs.html)) and vice versa. Unfortunately this involves three (!) different markdown parsers. If you're experiencing problems, use Pico's [`erusev/parsedown-extra`](https://github.com/erusev/parsedown-extra) as a reference. You can try to make the contents compatible to [Redcarpet](https://github.com/vmg/redcarpet) by yourself, otherwise please address the issues in your pull request message and we'll take care of it.
+
+Versioning
+----------
+
+Pico follows [Semantic Versioning 2.0](http://semver.org) and uses version numbers like `MAJOR`.`MINOR`.`PATCH`. We will increment the:
+
+- `MAJOR` version when we make incompatible API changes,
+- `MINOR` version when we add functionality in a backwards-compatible manner, and
+- `PATCH` version when we make backwards-compatible bug fixes.
+
+For more information please refer to the http://semver.org website.
+
+Branching
+---------
+
+The `master` branch contains the current development version of Pico. It is likely *unstable* and *not ready for production use*. However, the `master` branch always consists of a deployable version of Pico.
+
+Pico's actual development happens in separate development branches. Development branches are prefixed by:
+
+- `feature/` for bigger features,
+- `enhancement/` for smaller improvements, and
+- `bugfix/` for bug fixes.
+
+As soon as development reaches a point where feedback is appreciated, a [pull request](https://github.com/picocms/Pico/pulls) is opened. After some time (very soon for bug fixes, and other improvements should have a reasonable feedback phase) the pull request is merged into `master` and the development branch will be deleted.
+
+Build & Release process
+-----------------------
+
+This is work in progress. Please refer to [#268](https://github.com/picocms/Pico/issues/268) for details.
+
+<!--
+
+Defined below is a specification to which the Build and Release process of Pico should follow. We use `travis-ci` to automate the process, and each commit to `master` should be releasable.
+
+#### Commit phase
+- Commit changes
+- Create & Push Git tag
+- Trigger automatic build process...
+
+Example commit message:
+
+    Pico 1.0.1
+    * [New] ...
+    * [Changed] ...
+
+*Please submit pull-requests with a properly
+formatted commit message/SemVer increase to avoid the need for manual amendments.*
+
+#### Analysis phase
+- Run through `scrutinizer-ci`?
+
+#### Packaging phase
+- Run composer locally
+- Create a ZIP archive (so vendor/ is included)
+- Build documentation, output goes to a new folder in the `gh-pages` branch
+
+#### Release phase
+- Create new Git release at tag
+- Upload ZIP archive
+- Upload documentation to the `gh-pages` branch
+- Set Symlink for latest documentation (http://picocms.org/docs/latest)
+- Update release information on GitHub with:
+    - Release title (taken from changelog)
+    - Changelog
+
+#### Announcements
+- Where to announce new Pico release?
+
+-->

+ 0 - 0
license.txt → LICENSE


+ 94 - 32
README.md

@@ -1,58 +1,120 @@
-# Pico
+Pico
+====
 
 
-[![License](https://img.shields.io/packagist/l/doctrine/orm.svg)](https://scrutinizer-ci.com/g/theshka/Pico/build-status/LICENSE)
-[![Version](https://img.shields.io/badge/version-0.9-lightgrey.svg)]()
-[![Build Status](https://scrutinizer-ci.com/g/theshka/Pico/badges/build.png?b=master)](https://scrutinizer-ci.com/g/theshka/Pico/build-status/master) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/theshka/Pico/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/theshka/Pico/?branch=master)
+[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/picocms/Pico/blob/master/LICENSE)
+[![Version](https://img.shields.io/badge/version-1.0-lightgrey.svg)](https://github.com/picocms/Pico/releases/latest)
+[![Build Status](https://travis-ci.org/picocms/Pico.svg)](https://travis-ci.org/picocms/Pico)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/theshka/Pico/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/theshka/Pico/?branch=master)
 
 
 Pico is a stupidly simple, blazing fast, flat file CMS. See http://picocms.org/ for more info.
 Pico is a stupidly simple, blazing fast, flat file CMS. See http://picocms.org/ for more info.
 
 
 <!--flippa verify-->
 <!--flippa verify-->
 [![I Love Open Source](http://www.iloveopensource.io/images/logo-lightbg.png)](http://www.iloveopensource.io/projects/524c55dcca7964c617000756)
 [![I Love Open Source](http://www.iloveopensource.io/images/logo-lightbg.png)](http://www.iloveopensource.io/projects/524c55dcca7964c617000756)
 
 
-Requirements
----
-Requires PHP 5.3+
+Install
+-------
 
 
-Download
----
-You can install the latest version either by downloading it from <http://picocms.org/> or use Git:
+You can install Pico either using a pre-bundled release or with composer. Pico is also available on [Packagist.org][] and may be included in other projects via `composer require picocms/pico`. Pico requires PHP 5.3+
+
+#### Using a pre-bundled release
+
+Just [download the latest Pico release][LatestRelease] and upload all files to the `httpdocs` directory (e.g. `/var/www/html`) of your server.
+
+#### Composer
+
+###### Step 1 - for users
+[Download the *source code* of Pico's latest release][LatestRelease], upload all files to the `httpdocs` directory (e.g. `/var/www/html`) of your server and navigate to the upload directory using a shell.
 
 
+###### Step 1 - for developers
+Open a shell and navigate to the desired install directory of Pico within the `httpdocs` directory (e.g. `/var/www/html`) of your server. You can now clone Pico's Git repository as follows:
 ```shell
 ```shell
-git clone https://github.com/picocms/Pico.git
+$ git clone https://github.com/picocms/Pico.git .
 ```
 ```
+Please note that this gives you the current development version of Pico, what is likely *unstable* and *not ready for production use*!
 
 
-Install
----
-Download [composer](<https://getcomposer.org/>) and run it with install option.
+###### Step 2
+Download [composer][] and run it with the `install` option:
+```shell
+$ curl -sS https://getcomposer.org/installer | php
+$ php composer.phar install
+```
+
+Upgrade
+-------
+
+Upgrading Pico is very easy: You just have to replace all of Pico's files - that's it! Nevertheless you should *always* create a backup of your Pico installation before upgrading.
+
+Pico follows [Semantic Versioning 2.0][SemVer] and uses version numbers like `MAJOR`.`MINOR`.`PATCH`. When we update...
 
 
-    $ curl -sS https://getcomposer.org/installer | php
-    $ php composer.phar install
+- the `PATCH` version (e.g. `1.0.0` to `1.0.1`), we made backwards-compatible bug fixes. It's then sufficient to extract [Pico's latest release][LatestRelease] to your existing installation directory and overwriting all files.
+
+- the `MINOR` version (e.g. `1.0` to `1.1`), we added functionality in a backwards-compatible manner, but anyway recommend you to "install" Pico newly. Backup all of your files, empty your installation directory and install Pico as elucidated above. You can then copy your `config/config.php` and `content` directory without any change. If applicable, you can also copy the folder of your custom theme within the `themes` directory. Provided that you're using plugins, also copy all of your plugins from the `plugins` directory.
+
+- the `MAJOR` version (e.g. `1.0` to `2.0`), a appropriate upgrade tutorial will be provided.
+
+Upgrading Pico 0.8 or 0.9 to Pico 1.0 is a special case. The new `PicoDeprecated` plugin ensures backwards compatibility, so you basically can follow the above upgrade instructions as if we updated the `MINOR` version. However, we recommend you to take some further steps to confine the necessity of `PicoDeprecated` as far as possible. For more information about what has changed with Pico 1.0 and a step-by-step upgrade tutorial, please refer to the [upgrade page of our website][HelpUpgrade].
 
 
 Run
 Run
 ---
 ---
 
 
-The easiest way to Pico is using [the built-in web server on PHP](<http://php.net/manual/en/features.commandline.webserver.php>).
+You have nothing to consider specially, simply navigate to your Pico install using your favorite web browser. Pico's default contents will explain how to use your brand new, stupidly simple, blazing fast, flat file CMS.
 
 
-    $ php -S 0.0.0.0:8080
+#### You don't have a web server?
+Starting with PHP 5.4 the easiest way to try Pico is using [the built-in web server of PHP][PHPServer]. Please note that PHPs built-in web server is for development and testing purposes only!
 
 
-Pico will be accessible from <http://localhost:8080>.
+###### Step 1
+Navigate to Pico's installation directory using a shell.
 
 
-Getting Help
----
-You can read the wiki if you are looking for examples and read the inline-docs for more development information.
+###### Step 2
+Start PHPs built-in web server:
+```shell
+$ php -S 127.0.0.1:8080
+```
 
 
-If you find a bug please report it on the issues page, but remember to include as much detail as possible, and what someone can do to re-create the issue.
+###### Step 3
+Access Pico from http://localhost:8080.
 
 
-Issues with plugins should be reported on the offending plugins homepage, same goes for themes.
+Getting Help
+------------
 
 
-Contributing
----
-Help make PicoCMS better by checking out the GitHub repository and submitting pull requests.
+#### Getting Help as a user
+If you want to get started using Pico, please refer to our [user docs][HelpUserDocs]. Please read the [upgrade notes][HelpUpgrade] if you want to upgrade from Pico 0.8 or 0.9 to Pico 1.0. You can find officially supported plugins and themes on [our website][OfficialPlugins]. A greater choice of third-party plugins and themes can be found in our [Wiki][] on the [plugins][WikiPlugins] or [themes][WikiThemes] pages respectively. If you want to create your own plugin or theme, please refer to the "Getting Help as a developer" section below.
 
 
-If you create a plugin please add it to the Wiki.
+#### Getting Help as a developer
+If you're a developer, please refer to the "Contributing" section below and our [contribution guidelines][ContributionGuidelines]. To get you started with creating a plugin or theme, please read the [dev docs on our website][HelpDevDocs].
 
 
-Plugins + Wiki
----
-Pico can be extended with a wide variety of plugins in order to add extra functionality, speed, or features.
+#### You still need help or experience a problem with Pico?
+When the docs can't answer your question or when you're experiencing problems with Pico, please don't hesitate to create a new [Issue][Issues] on GitHub. Concerning problems with plugins or themes, please refer to the website of the developer of this plugin or theme.
+
+**Before creating a new Issue,** please make sure the problem wasn't reported yet using [GitHubs search engine][IssuesSearch]. Please describe your issue as clear as possible and always include steps to reproduce the problem.
 
 
-Visit the [Pico Wiki](https://github.com/picocms/Pico/wiki) for docs, plugins, themes, etc...
+Contributing
+------------
+
+You want to contribute to Pico? We really appreciate that! You can help make Pico better by [contributing code][PullRequests] or [reporting issues][Issues], but please take note of our [contribution guidelines][ContributionGuidelines]. In general you can contribute in three different areas:
+
+1. Plugins & Themes: You're a plugin developer or theme designer? We love you guys! You can find tons of information about how to develop plugins and themes at http://picocms.org/plugin-dev.html. If you have created a plugin or theme, please add it to our [Wiki][], either on the [plugins][WikiPlugins] or [themes page][WikiThemes]. Doing so, we may select and promote your plugin or theme on [our website][OfficialPlugins] as officially supported!
+
+2. Documentation: We always appreciate people improving our documentation. You can either improve the [inline user docs][EditInlineDocs] or the more extensive [user docs on our website][EditUserDocs]. You can also improve the [docs for plugin and theme developers][EditDevDocs]. Simply fork Pico from https://github.com/picocms/Pico, change the Markdown files and open a [pull request][PullRequests].
+
+3. Pico's Core: The supreme discipline is to work on Pico's Core. Your contribution should help *every* Pico user to have a better experience with Pico. If this is the case, fork Pico from https://github.com/picocms/Pico and open a [pull request][PullRequests]. We look forward to your contribution!
+
+[Packagist.org]: http://packagist.org/packages/picocms/pico
+[LatestRelease]: https://github.com/picocms/Pico/releases/latest
+[composer]: https://getcomposer.org/
+[SemVer]: http://semver.org
+[PHPServer]: http://php.net/manual/en/features.commandline.webserver.php
+[HelpUpgrade]: http://picocms.org/upgrade.html
+[HelpUserDocs]: http://picocms.org/docs.html
+[HelpDevDocs]: http://picocms.org/plugin-dev.html
+[OfficialPlugins]: http://picocms.org/plugins.html
+[Wiki]: https://github.com/picocms/Pico/wiki
+[WikiPlugins]: https://github.com/picocms/Pico/wiki/Pico-Plugins
+[WikiThemes]: https://github.com/picocms/Pico/wiki/Pico-Themes
+[Issues]: https://github.com/picocms/Pico/issues
+[IssuesSearch]: https://github.com/picocms/Pico/search?type=Issues
+[PullRequests]: https://github.com/picocms/Pico/pulls
+[ContributionGuidelines]: https://github.com/picocms/Pico/blob/master/CONTRIBUTING.md
+[EditInlineDocs]: https://github.com/picocms/Pico/blob/master/content-sample/index.md
+[EditUserDocs]: https://github.com/picocms/Pico/tree/gh-pages/_docs
+[EditDevDocs]: https://github.com/picocms/Pico/tree/gh-pages/_plugin-dev

+ 0 - 70
changelog.txt

@@ -1,70 +0,0 @@
-*** Pico Changelog ***
-
-2015.04.28 - version 0.9
- * [New] Default theme is now mobile-friendly
- * [New] Description meta now available in content areas
- * [Changed] content folder is now content-sample
- * [Changed] Updated documentation & wiki
- * [Changed] Removed Composer, Twig files in /vendor, you must run composer install now
- * [Changed] Localized date format; strftime() instead of date()
- * [Changed] Added ignore for tmp file extensions in the get_files() method
- * [Fixed] Pico now only removes the 1st comment block in .md file 
- * [Fixed] Issue wherein the alphabetical sorting of pages did not happen
-
-2013.10.23 - version 0.8
- * [New] Added ability to set template in content meta
- * [New] Added before_parse_content and after_parse_content hooks
- * [Changed] content_parsed hook is now depreciated
- * [Changed] Moved loading the config to nearer the beginning of the class
- * [Changed] Only append ellipsis in limit_words() when word count exceeds max
- * [Changed] Made private methods protected for better inheritance
- * [Fixed] Fixed get_protocol() method to work in more situations
-
-2013.09.04 - version 0.7
- * [New] Added before_read_file_meta and get_page_data plugin hooks to customize page meta data
- * [Changed] Make get_files() ignore dotfiles
- * [Changed] Make get_pages() ignore Emacs and temp files
- * [Changed] Use composer version of Markdown
- * [Changed] Other small tweaks
- * [Fixed] Date warnings and other small bugs
-
-2013.05.07 - version 0.6.2
- * [Changed] Replaced glob_recursive with get_files
-
-2013.05.07 - version 0.6.1
- * [New] Added "content" and "excerpt" fields to pages
- * [New] Added excerpt_length config setting
-
-2013.05.06 - version 0.6
- * [New] Added plugin functionality
- * [Changed] Other small cleanup
-
-2013.05.03 - version 0.5
- * [New] Added ability to order pages by "alpha" or "date" (asc or desc)
- * [New] Added prev_page, current_page, next_page and is_front_page template vars
- * [New] Added "Author" and "Date" title meta fields
- * [Changed] Added "twig_config" to settings
- * [Changed] Updated documentation
- * [Fixed] Query string 404 bug
-
-2013.05.01 - version 0.4.1
- * [New] Added CONTENT_EXT global
- * [Changed] Use .md files instead of .txt
-
-2013.05.01 - version 0.4
- * [New] Add get_pages() function for listing content
- * [New] Added changelog.txt
- * [Changed] Updated default theme
- * [Changed] Updated documentation
-
-2013.04.27 - version 0.3
- * [Fixed] get_config() function
-
-2013.04.26 - version 0.2
- * [Changed] Updated Twig
- * [Changed] Better checking for HTTPS
- * [Fixed] Add 404 header to 404 page
- * [Fixed] Case sensitive folder bug
-
-2012.04.04 - version 0.1
- * Initial release

+ 8 - 3
composer.json

@@ -12,11 +12,16 @@
         }
         }
     ],
     ],
     "require": {
     "require": {
-        "php": ">=5.3.2",
+        "php": ">=5.3.6",
         "twig/twig": "1.18.*",
         "twig/twig": "1.18.*",
-        "erusev/parsedown-extra": "dev-master@dev"
+        "erusev/parsedown-extra": "0.7.*",
+        "symfony/yaml" : "2.3"
     },
     },
     "autoload": {
     "autoload": {
-        "files": ["lib/pico.php"]
+        "psr-0": {
+            "Pico": "lib/",
+            "PicoPluginInterface": "lib/",
+            "AbstractPicoPlugin": "lib/"
+        }
     }
     }
 }
 }

+ 27 - 23
config/config.php.template

@@ -1,18 +1,19 @@
 <?php
 <?php
 /**
 /**
- * Pico Configuration
+ * Pico configuration
  *
  *
- *  This is the configuration file for Pico. It comes loaded with the
- *  default values, which can be found in the get_config() method of
- *  the Pico class (lib/pico.php).
- *
- * @author Gilbert Pellegrom
- * @link http://picocms.org
- * @license http://opensource.org/licenses/MIT
- * @version 0.9
+ * This is the configuration file for {@link Pico}. It comes loaded with the
+ * default values, which can be found in {@link Pico::getConfig()} (see
+ * {@path "lib/Pico.php"}).
  *
  *
  * To override any of the default settings below, copy this file to
  * To override any of the default settings below, copy this file to
- * `config.php`, uncomment the line and make and save your changes.
+ * {@path "config/config.php"}, uncomment the line, then make and
+ * save your changes.
+ *
+ * @author  Gilbert Pellegrom
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
  */
  */
 
 
 /*
 /*
@@ -20,35 +21,38 @@
  */
  */
 // $config['site_title'] = 'Pico';              // Site title
 // $config['site_title'] = 'Pico';              // Site title
 // $config['base_url'] = '';                    // Override base URL (e.g. http://example.com)
 // $config['base_url'] = '';                    // Override base URL (e.g. http://example.com)
+// $config['rewrite_url'] = null;               // A boolean indicating forced URL rewriting
 
 
 /*
 /*
  * THEME
  * THEME
  */
  */
 // $config['theme'] = 'default';                // Set the theme (defaults to "default")
 // $config['theme'] = 'default';                // Set the theme (defaults to "default")
 // $config['twig_config'] = array(              // Twig settings
 // $config['twig_config'] = array(              // Twig settings
-//	'cache' => false,	                        // To enable Twig caching change this to CACHE_DIR
-//	'autoescape' => false,                      // Autoescape Twig vars
-//	'debug' => false                            // Enable Twig debug
+//     'cache' => false,                        // To enable Twig caching change this to a path to a writable directory
+//     'autoescape' => false,                   // Auto-escape Twig vars
+//     'debug' => false                         // Enable Twig debug
 // );
 // );
 
 
 /*
 /*
  * CONTENT
  * CONTENT
  */
  */
-// $config['date_format'] = '%D %T';             // Set the PHP date format as described here: http://php.net/manual/en/function.strftime.php
-// $config['pages_order_by'] = 'alpha';           // Order pages by "alpha" or "date"
-// $config['pages_order'] = 'asc';                // Order pages "asc" or "desc"
-// $config['excerpt_length'] = 50;                // The pages excerpt length (in words)
-// $config['content_dir'] = 'content-sample/';    // Content directory
+// $config['date_format'] = '%D %T';            // Set the PHP date format as described here: http://php.net/manual/en/function.strftime.php
+// $config['pages_order_by'] = 'alpha';         // Order pages by "alpha" or "date"
+// $config['pages_order'] = 'asc';              // Order pages "asc" or "desc"
+// $config['content_dir'] = 'content-sample/';  // Content directory
+// $config['content_ext'] = '.md';              // File extension of content files to serve
 
 
 /*
 /*
  * TIMEZONE
  * TIMEZONE
  */
  */
-// date_default_timezone_set('UTC');              // Timezone may be reqired by your php install
+// $config['timezone'] = 'UTC';                 // Timezone may be required by your php install
 
 
 /*
 /*
- * CUSTOM
+ * PLUGINS
  */
  */
-// $config['custom_setting'] = 'Hello';           // Can be accessed by {{ config.custom_setting }} in a theme
+// $config['DummyPlugin.enabled'] = false;      // Force DummyPlugin to be disabled
 
 
-// Keep this line
-return $config;
+/*
+ * CUSTOM
+ */
+// $config['custom_setting'] = 'Hello';         // Can be accessed by {{ config.custom_setting }} in a theme

+ 3 - 3
content-sample/404.md

@@ -1,9 +1,9 @@
-/*
+---
 Title: Error 404
 Title: Error 404
 Robots: noindex,nofollow
 Robots: noindex,nofollow
-*/
+---
 
 
 Error 404
 Error 404
 =========
 =========
 
 
-Woops. Looks like this page doesn't exist.
+Woops. Looks like this page doesn't exist.

+ 0 - 0
content-sample/index.html


+ 250 - 73
content-sample/index.md

@@ -1,109 +1,286 @@
-/*
+---
 Title: Welcome
 Title: Welcome
-Description: This description will go in the meta description tag
-*/
+Description: Pico is a stupidly simple, blazing fast, flat file CMS.
+---
 
 
 ## Welcome to Pico
 ## Welcome to Pico
 
 
-Congratulations, you have successfully installed [Pico](http://picocms.org/). Pico is a stupidly simple, blazing fast, flat file CMS.
+Congratulations, you have successfully installed [Pico](http://picocms.org/).
+%meta.description% <!-- replaced by the above Description meta header -->
 
 
-### Creating Content
+## Creating Content
 
 
-Pico is a flat file CMS, this means there is no administration backend and database to deal with. You simply create `.md` files in the "content-sample"
-folder and that becomes a page. For example, this file is called `index.md` and is shown as the main landing page. 
+Pico is a flat file CMS, this means there is no administration backend or
+database to deal with. You simply create `.md` files in the `content-sample`
+folder and that becomes a page. For example, this file is called `index.md`
+and is shown as the main landing page.
 
 
-If you create a folder within the content-sample folder (e.g. `content-sample/sub`) and put an `index.md` inside it, you can access that folder at the URL 
-`http://yoursite.com/sub`. If you want another page within the sub folder, simply create a text file with the corresponding name (e.g. `content-sample/sub/page.md`)
-and you will be able to access it from the URL `http://yoursite.com/sub/page`. Below we've shown some examples of content-sample locations and their corresponing URL's:
+If you create a folder within the content folder (e.g. `content-sample/sub`)
+and put an `index.md` inside it, you can access that folder at the URL
+`http://example.com/pico/?sub`. If you want another page within the sub folder,
+simply create a text file with the corresponding name and you will be able to
+access it (e.g. `content-sample/sub/page.md` is accessible from the URL
+`http://example.com/pico/?sub/page`). Below we've shown some examples of
+locations and their corresponding URLs:
 
 
-<table>
-	<thead>
-		<tr><th>Physical Location</th><th>URL</th></tr>
-	</thead>
-	<tbody>
-		<tr><td>content-sample/index.md</td><td>/</td></tr>
-		<tr><td>content-sample/sub.md</td><td>/sub</td></tr>
-		<tr><td>content-sample/sub/index.md</td><td>/sub (same as above)</td></tr>
-		<tr><td>content-sample/sub/page.md</td><td>/sub/page</td></tr>
-		<tr><td>content-sample/a/very/long/url.md</td><td>/a/very/long/url</td></tr>
-	</tbody>
+<table style="width: 100%; max-width: 40em;">
+    <thead>
+        <tr>
+            <th style="width: 50%;">Physical Location</th>
+            <th style="width: 50%;">URL</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td>content-sample/index.md</td>
+            <td><a href="%base_url%">/</a></td>
+        </tr>
+        <tr>
+            <td>content-sample/sub.md</td>
+            <td><del>?sub</del> (not accessible, see below)</td>
+        </tr>
+        <tr>
+            <td>content-sample/sub/index.md</td>
+            <td><a href="%base_url%?sub">?sub</a> (same as above)</td>
+        </tr>
+        <tr>
+            <td>content-sample/sub/page.md</td>
+            <td><a href="%base_url%?sub/page">?sub/page</a></td>
+        </tr>
+        <tr>
+            <td>content-sample/a/very/long/url.md</td>
+            <td><a href="%base_url%?a/very/long/url">?a/very/long/url</a> (doesn't exist)</td>
+        </tr>
+    </tbody>
 </table>
 </table>
 
 
-If a file cannot be found, the file `content-sample/404.md` will be shown.
+If a file cannot be found, the file `content-sample/404.md` will be shown. You
+can add `404.md` files to any directory, so if you want to use a special error
+page for your blog, simply create `content-sample/blog/404.md`.
+
+Instead of adding your own content to the `content-sample` folder, you should
+create your own `content` directory in Pico's root directory. You can then add
+and access your contents as described above.
 
 
 ### Text File Markup
 ### Text File Markup
 
 
-Text files are marked up using [Markdown](http://daringfireball.net/projects/markdown/syntax). They can also contain regular HTML.
+Text files are marked up using [Markdown][]. They can also contain regular HTML.
 
 
-At the top of text files you can place a block comment and specify certain attributes of the page. For example:
+At the top of text files you can place a block comment and specify certain
+attributes of the page. For example:
 
 
-	/*
-	Title: Welcome
-	Description: This description will go in the meta description tag
-	Author: Joe Bloggs
-	Date: 2013/01/01
-	Robots: noindex,nofollow
-	*/
+    ---
+    Title: Welcome
+    Description: This description will go in the meta description tag
+    Author: Joe Bloggs
+    Date: 2013/01/01
+    Robots: noindex,nofollow
+    Template: index
+    ---
 
 
-These values will be contained in the `{{ meta }}` variable in themes (see below).
+These values will be contained in the `{{ meta }}` variable in themes
+(see below).
 
 
 There are also certain variables that you can use in your text files:
 There are also certain variables that you can use in your text files:
 
 
-* <code>&#37;base_url&#37;</code> - The URL to your Pico site
+* <code>&#37;site_title&#37;</code> - The title of your Pico site
+* <code>&#37;base_url&#37;</code> - The URL to your Pico site; internal links
+  can be specified using <code>&#37;base_url&#37;?sub/page</code>
+* <code>&#37;theme_url&#37;</code> - The URL to the currently used theme
+* <code>&#37;meta.*&#37;</code> - Access any meta variable of the current page,
+  e.g. <code>&#37;meta.author&#37;</code> is replaced with `Joe Bloggs`
+
+### Blogging
+
+Pico is not blogging software - but makes it very easy for you to use it as a
+blog. You can find many plugins out there implementing typical blogging
+features like authentication, tagging, pagination and social plugins. See the
+below Plugins section for details.
+
+If you want to use Pico as a blogging software, you probably want to do
+something like the following:
+1. Put all your blog articles in a separate `blog` folder in your `content`
+   directory. All these articles should have both a `Date` and `Template` meta
+   header, the latter with e.g. `blog-post` as value (see Step 2).
+2. Create a new Twig template called `blog-post.twig` (this must match the
+   `Template` meta header from Step 1) in your theme directory. This template
+   probably isn't very different from your default `index.twig`, it specifies
+   how your article pages will look like.
+3. Create a `blog.md` in your `content` folder and set its `Template` meta
+   header to e.g. `blog`. Also create a `blog.twig` in your theme directory.
+   This template will show a list of your articles, so you probably want to
+   do something like this:
+   ```
+    {% for page in pages %}
+        {% if page.id starts with "blog/" %}
+            <div class="post">
+                <h3><a href="{{ page.url }}">{{ page.title }}</a></h3>
+                <p class="date">{{ page.date_formatted }}</p>
+                <p class="excerpt">{{ page.description }}</p>
+            </div>
+        {% endif %}
+    {% endfor %}
+   ```
+4. Let Pico sort pages by date by setting `$config['pages_order_by'] = 'date';`
+   in your `config/config.php`. To use a descending order (newest articles
+   first), also add `$config['pages_order'] = 'desc';`. The former won't affect
+   pages without a `Date` meta header, but the latter does. To use ascending
+   order for your page navigation again, add Twigs `reverse` filter to the
+   navigation loop (`{% for page in pages|reverse %}...{% endfor %}`) in your
+   themes `index.twig`.
+5. Make sure to exclude the blog articles from your page navigation. You can
+   achieve this by adding `{% if not page starts with "blog/" %}...{% endif %}`
+   to the navigation loop.
+
+## Customization
+
+Pico is highly customizable in two different ways: On the one hand you can
+change Pico's appearance by using themes, on the other hand you can add new
+functionality by using plugins. Doing the former includes changing Pico's HTML,
+CSS and JavaScript, the latter mostly consists of PHP programming.
+
+This is all Greek to you? Don't worry, you don't have to spend time on these
+techie talk - it's very easy to use one of the great themes or plugins others
+developed and released to the public. Please refer to the next sections for
+details.
 
 
 ### Themes
 ### Themes
 
 
-You can create themes for your Pico installation in the "themes" folder. Check out the default theme for an example of a theme. Pico uses
-[Twig](http://twig.sensiolabs.org/documentation) for it's templating engine. You can select your theme by setting the `$config['theme']` variable
-in `config/config.php` to your theme folder.
+You can create themes for your Pico installation in the `themes` folder. Check
+out the default theme for an example. Pico uses [Twig][] for template
+rendering. You can select your theme by setting the `$config['theme']` option
+in `config/config.php` to the name of your theme folder.
 
 
-All themes must include an `index.html` file to define the HTML structure of the theme. Below are the Twig variables that are available to use in your theme:
+All themes must include an `index.twig` (or `index.html`) file to define the
+HTML structure of the theme. Below are the Twig variables that are available
+to use in your theme. Please note that paths (e.g. `{{ base_dir }}`) and URLs
+(e.g. `{{ base_url }}`) don't have a trailing slash.
 
 
-* `{{ config }}` - Conatins the values you set in `config/config.php` (e.g. `{{ config.theme }}` = "default")
+* `{{ config }}` - Conatins the values you set in `config/config.php`
+                   (e.g. `{{ config.theme }}` becomes `default`)
 * `{{ base_dir }}` - The path to your Pico root directory
 * `{{ base_dir }}` - The path to your Pico root directory
-* `{{ base_url }}` - The URL to your Pico site
-* `{{ theme_dir }}` - The path to the Pico active theme directory
-* `{{ theme_url }}` - The URL to the Pico active theme directory
-* `{{ site_title }}` - Shortcut to the site title (defined in `config/config.php`)
+* `{{ base_url }}` - The URL to your Pico site; use Twigs `link` filter to
+                     specify internal links (e.g. `{{ "sub/page"|link }}`),
+                     this guarantees that your link works whether URL rewriting
+                     is enabled or not
+* `{{ theme_dir }}` - The path to the currently active theme
+* `{{ theme_url }}` - The URL to the currently active theme
+* `{{ rewrite_url }}` - A boolean flag indicating enabled/disabled URL rewriting
+* `{{ site_title }}` - Shortcut to the site title (see `config/config.php`)
 * `{{ meta }}` - Contains the meta values from the current page
 * `{{ meta }}` - Contains the meta values from the current page
-	* `{{ meta.title }}`
-	* `{{ meta.description }}`
-	* `{{ meta.author }}`
-	* `{{ meta.date }}`
-	* `{{ meta.date_formatted }}`
-	* `{{ meta.robots }}`
-* `{{ content-sample }}` - The content-sample of the current page (after it has been processed through Markdown)
-* `{{ pages }}` - A collection of all the content-sample in your site
-	* `{{ page.title }}`
-	* `{{ page.url }}`
-	* `{{ page.author }}`
-	* `{{ page.date }}`
-	* `{{ page.date_formatted }}`
-	* `{{ page.content-sample }}`
-	* `{{ page.excerpt }}`
-* `{{ prev_page }}` - A page object of the previous page (relative to current_page)
-* `{{ current_page }}` - A page object of the current_page
-* `{{ next_page }}` - A page object of the next page (relative to current_page)
+    * `{{ meta.title }}`
+    * `{{ meta.description }}`
+    * `{{ meta.author }}`
+    * `{{ meta.date }}`
+    * `{{ meta.date_formatted }}`
+    * `{{ meta.time }}`
+    * `{{ meta.robots }}`
+    * ...
+* `{{ content }}` - The content of the current page
+                    (after it has been processed through Markdown)
+* `{{ pages }}` - A collection of all the content pages in your site
+    * `{{ page.id }}` - The relative path to the content file (unique ID)
+    * `{{ page.url }}` - The URL to the page
+    * `{{ page.title }}` - The title of the page (YAML header)
+    * `{{ page.description }}` - The description of the page (YAML header)
+    * `{{ page.author }}` - The author of the page (YAML header)
+    * `{{ page.time }}` - The timestamp derived from the `Date` header
+    * `{{ page.date }}` - The date of the page (YAML header)
+    * `{{ page.date_formatted }}` - The formatted date of the page
+    * `{{ page.raw_content }}` - The raw, not yet parsed contents of the page;
+                                 use Twigs `content` filter to get the parsed
+                                 contents of a page by passing its unique ID
+                                 (e.g. `{{ "sub/page"|content }}`)
+    * `{{ page.meta }}`- The meta values of the page
+* `{{ prev_page }}` - The data of the previous page (relative to `current_page`)
+* `{{ current_page }}` - The data of the current page
+* `{{ next_page }}` - The data of the next page (relative to `current_page`)
 * `{{ is_front_page }}` - A boolean flag for the front page
 * `{{ is_front_page }}` - A boolean flag for the front page
 
 
-Pages can be used like:
+Pages can be used like the following:
+
+    <ul class="nav">
+        {% for page in pages %}
+            <li><a href="{{ page.url }}">{{ page.title }}</a></li>
+        {% endfor %}
+    </ul>
 
 
-<pre>&lt;ul class=&quot;nav&quot;&gt;
-	{% for page in pages %}
-	&lt;li&gt;&lt;a href=&quot;{{ page.url }}&quot;&gt;{{ page.title }}&lt;/a&gt;&lt;/li&gt;
-	{% endfor %}
-&lt;/ul&gt;</pre>
+You can use different templates for different content files by specifying the
+`Template` meta header. Simply add e.g. `Template: blog-post` to a content file
+and Pico will use the `blog-post.twig` file in your theme folder to render
+the page.
+
+You don't have to create your own theme if Pico's default theme isn't
+sufficient for you, you can use one of the great themes third-party developers
+and designers created in the past. As with plugins, you can find themes in
+[our Wiki][WikiThemes].
 
 
 ### Plugins
 ### Plugins
 
 
-See [http://pico.dev7studios.com/plugins](http://picocms.org/plugins)
+#### Plugins for users
+
+Officially tested plugins can be found at http://picocms.org/plugins.html, but
+there are many awesome third-party plugins out there! A good start point for
+discovery is [our Wiki][WikiPlugins].
+
+Pico makes it very easy for you to add new features to your website. Simply
+upload the files of the plugin to the `plugins/` directory and you're done.
+Depending on the plugin you've installed, you may have to go through some more
+steps (e.g. specifying config variables), the plugin docs or `README` file will
+explain what to do.
+
+Plugins which were written to work with Pico 1.0 can be enabled and disabled
+through your `config/config.php`. If you want to e.g. disable the `PicoExcerpt`
+plugin, add the following line to your `config/config.php`:
+`$config['PicoExcerpt.enabled'] = false;`. To force the plugin to be enabled
+replace `false` with `true`.
+
+#### Plugins for developers
+
+You're a plugin developer? We love you guys! You can find tons of information
+about how to develop plugins at http://picocms.org/plugin-dev.html. If you've
+developed a plugin for Pico 0.9 or older, you probably want to upgrade it
+to the brand new plugin system introduced with Pico 1.0. Please refer to the
+[upgrade section of the docs][PluginUpgrade].
+
+## Config
+
+You can override the default Pico settings (and add your own custom settings)
+by editing `config/config.php` in the Pico directory. For a brief overview of
+the available settings and their defaults see `config/config.php.template`. To
+override a setting, copy `config/config.php.template` to `config/config.php`,
+uncomment the setting and set your custom value.
+
+### URL Rewriting
+
+Pico's default URLs (e.g. %base_url%/?sub/page) already are very user-friendly.
+Additionally, Pico offers you a URL rewrite feature to make URLs even more
+user-friendly (e.g. %base_url%/sub/page).
+
+If you're using the Apache web server, URL rewriting probably already is
+enabled - try it yourself, click on the [second URL](%base_url%/sub/page). If
+you get an error message from your web server, please make sure to enable the
+[`mod_rewrite` module][ModRewrite]. Assuming the second URL works, but Pico
+still shows no rewritten URLs, force URL rewriting by setting
+`$config['rewrite_url'] = true;` in your `config/config.php`.
+
+If you're using Nginx, you can use the following configuration to enable
+URL rewriting. Don't forget to adjust the path (`/pico/`; line `1` and `4`)
+to match your installation directory. You can then enable URL rewriting by
+setting `$config['rewrite_url'] = true;` in your `config/config.php`.
 
 
-### Config
+    location /pico/ {
+        index index.php;
+        try_files $uri $uri/ /pico/?$uri&$args;
+    }
 
 
-You can override the default Pico settings (and add your own custom settings) by editing `config/config.php` in the Pico directory.
-The `config/config.php.template` lists all of the settings and their defaults. To override a setting simply copy
-`config/config.php.template` to `config/config.php`, uncomment the setting and set your custom value.
+## Documentation
 
 
-### Documentation
+For more help have a look at the Pico documentation at http://picocms.org/docs.
 
 
-For more help have a look at the Pico documentation at [http://picocms.org/docs](http://picocms.org/docs)
+[Markdown]: http://daringfireball.net/projects/markdown/syntax
+[Twig]: http://twig.sensiolabs.org/documentation
+[WikiThemes]: https://github.com/picocms/Pico/wiki/Pico-Themes
+[WikiPlugins]: https://github.com/picocms/Pico/wiki/Pico-Plugins
+[PluginUpgrade]: http://picocms.org/plugin-dev.html#upgrade
+[ModRewrite]: https://httpd.apache.org/docs/current/mod/mod_rewrite.html

+ 3 - 3
content-sample/sub/index.md

@@ -1,10 +1,10 @@
-/*
+---
 Title: Sub Page Index
 Title: Sub Page Index
-*/
+---
 
 
 ## This is a Sub Page Index
 ## This is a Sub Page Index
 
 
-This is index.md in the "sub" folder.
+This is `index.md` in the `sub` folder.
 
 
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies tristique nulla et mattis. Phasellus id massa eget nisl congue blandit sit amet id ligula. Praesent et nulla eu augue tempus sagittis. Mauris faucibus nibh et nibh cursus in vestibulum sapien egestas. Curabitur ut lectus tortor. Sed ipsum eros, egestas ut eleifend non, elementum vitae eros. Mauris felis diam, pellentesque vel lacinia ac, dictum a nunc. Mauris mattis nunc sed mi sagittis et facilisis tortor volutpat. Etiam tincidunt urna mattis erat placerat placerat ac eu tellus. Ut nec velit id nisl tincidunt vehicula id a metus. Pellentesque erat neque, faucibus id ultricies vel, mattis in ante. Donec lobortis, mauris id congue scelerisque, diam nisl accumsan orci, condimentum porta est magna vel arcu. Curabitur varius ante dui. Vivamus sit amet ante ac diam ullamcorper sodales sed a odio.
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies tristique nulla et mattis. Phasellus id massa eget nisl congue blandit sit amet id ligula. Praesent et nulla eu augue tempus sagittis. Mauris faucibus nibh et nibh cursus in vestibulum sapien egestas. Curabitur ut lectus tortor. Sed ipsum eros, egestas ut eleifend non, elementum vitae eros. Mauris felis diam, pellentesque vel lacinia ac, dictum a nunc. Mauris mattis nunc sed mi sagittis et facilisis tortor volutpat. Etiam tincidunt urna mattis erat placerat placerat ac eu tellus. Ut nec velit id nisl tincidunt vehicula id a metus. Pellentesque erat neque, faucibus id ultricies vel, mattis in ante. Donec lobortis, mauris id congue scelerisque, diam nisl accumsan orci, condimentum porta est magna vel arcu. Curabitur varius ante dui. Vivamus sit amet ante ac diam ullamcorper sodales sed a odio.
 
 

+ 3 - 3
content-sample/sub/page.md

@@ -1,10 +1,10 @@
-/*
+---
 Title: Sub Page
 Title: Sub Page
-*/
+---
 
 
 ## This is a Sub Page
 ## This is a Sub Page
 
 
-This is page.md in the "sub" folder.
+This is `page.md` in the `sub` folder.
 
 
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies tristique nulla et mattis. Phasellus id massa eget nisl congue blandit sit amet id ligula. Praesent et nulla eu augue tempus sagittis. Mauris faucibus nibh et nibh cursus in vestibulum sapien egestas. Curabitur ut lectus tortor. Sed ipsum eros, egestas ut eleifend non, elementum vitae eros. Mauris felis diam, pellentesque vel lacinia ac, dictum a nunc. Mauris mattis nunc sed mi sagittis et facilisis tortor volutpat. Etiam tincidunt urna mattis erat placerat placerat ac eu tellus. Ut nec velit id nisl tincidunt vehicula id a metus. Pellentesque erat neque, faucibus id ultricies vel, mattis in ante. Donec lobortis, mauris id congue scelerisque, diam nisl accumsan orci, condimentum porta est magna vel arcu. Curabitur varius ante dui. Vivamus sit amet ante ac diam ullamcorper sodales sed a odio.
 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ultricies tristique nulla et mattis. Phasellus id massa eget nisl congue blandit sit amet id ligula. Praesent et nulla eu augue tempus sagittis. Mauris faucibus nibh et nibh cursus in vestibulum sapien egestas. Curabitur ut lectus tortor. Sed ipsum eros, egestas ut eleifend non, elementum vitae eros. Mauris felis diam, pellentesque vel lacinia ac, dictum a nunc. Mauris mattis nunc sed mi sagittis et facilisis tortor volutpat. Etiam tincidunt urna mattis erat placerat placerat ac eu tellus. Ut nec velit id nisl tincidunt vehicula id a metus. Pellentesque erat neque, faucibus id ultricies vel, mattis in ante. Donec lobortis, mauris id congue scelerisque, diam nisl accumsan orci, condimentum porta est magna vel arcu. Curabitur varius ante dui. Vivamus sit amet ante ac diam ullamcorper sodales sed a odio.
 
 

+ 13 - 11
index.php

@@ -1,15 +1,17 @@
 <?php
 <?php
+// load dependencies
+require_once(__DIR__ . '/vendor/autoload.php');
 
 
-define('ROOT_DIR', realpath(dirname(__FILE__)) . '/');
-define('LIB_DIR', ROOT_DIR . 'lib/');
-define('VENDOR_DIR', ROOT_DIR . 'vendor/');
-define('PLUGINS_DIR', ROOT_DIR . 'plugins/');
-define('THEMES_DIR', ROOT_DIR . 'themes/');
-define('CONFIG_DIR', ROOT_DIR . 'config/');
-define('CACHE_DIR', LIB_DIR . 'cache/');
+// instance Pico
+$pico = new Pico(
+    __DIR__,    // root dir
+    'config/',  // config dir
+    'plugins/', // plugins dir
+    'themes/'   // themes dir
+);
 
 
-define('CONTENT_EXT', '.md');
+// override configuration?
+// $pico->setConfig(array());
 
 
-require_once(VENDOR_DIR . 'autoload.php');
-require_once(LIB_DIR . 'pico.php');
-$pico = new Pico();
+// run application
+echo $pico->run();

+ 255 - 0
lib/AbstractPicoPlugin.php

@@ -0,0 +1,255 @@
+<?php
+
+/**
+ * Abstract class to extend from when implementing a Pico plugin
+ *
+ * @see PicoPluginInterface
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+abstract class AbstractPicoPlugin implements PicoPluginInterface
+{
+    /**
+     * Current instance of Pico
+     *
+     * @see PicoPluginInterface::getPico()
+     * @var Pico
+     */
+    private $pico;
+
+    /**
+     * Boolean indicating if this plugin is enabled (true) or disabled (false)
+     *
+     * @see PicoPluginInterface::isEnabled()
+     * @see PicoPluginInterface::setEnabled()
+     * @var boolean
+     */
+    protected $enabled = true;
+
+    /**
+     * Boolean indicating if this plugin was ever enabled/disabled manually
+     *
+     * @see PicoPluginInterface::isStatusChanged()
+     * @var boolean
+     */
+    protected $statusChanged = false;
+
+    /**
+     * List of plugins which this plugin depends on
+     *
+     * @see AbstractPicoPlugin::checkDependencies()
+     * @see PicoPluginInterface::getDependencies()
+     * @var string[]
+     */
+    protected $dependsOn = array();
+
+    /**
+     * List of plugin which depend on this plugin
+     *
+     * @see AbstractPicoPlugin::checkDependants()
+     * @see PicoPluginInterface::getDependants()
+     * @var object[]
+     */
+    private $dependants;
+
+    /**
+     * @see PicoPluginInterface::__construct()
+     */
+    public function __construct(Pico $pico)
+    {
+        $this->pico = $pico;
+    }
+
+    /**
+     * @see PicoPluginInterface::handleEvent()
+     */
+    public function handleEvent($eventName, array $params)
+    {
+        // plugins can be enabled/disabled using the config
+        if ($eventName === 'onConfigLoaded') {
+            $pluginEnabled = $this->getConfig(get_called_class() . '.enabled');
+            if ($pluginEnabled !== null) {
+                $this->setEnabled($pluginEnabled);
+            } else {
+                $pluginConfig = $this->getConfig(get_called_class());
+                if (is_array($pluginConfig) && isset($pluginConfig['enabled'])) {
+                    $this->setEnabled($pluginConfig['enabled']);
+                }
+            }
+        }
+
+        if ($this->isEnabled() || ($eventName === 'onPluginsLoaded')) {
+            if (method_exists($this, $eventName)) {
+                call_user_func_array(array($this, $eventName), $params);
+            }
+        }
+    }
+
+    /**
+     * @see PicoPluginInterface::setEnabled()
+     */
+    public function setEnabled($enabled, $recursive = true, $auto = false)
+    {
+        $this->statusChanged = (!$this->statusChanged) ? !$auto : true;
+        $this->enabled = (bool) $enabled;
+
+        if ($enabled) {
+            $this->checkDependencies($recursive);
+        } else {
+            $this->checkDependants($recursive);
+        }
+    }
+
+    /**
+     * @see PicoPluginInterface::isEnabled()
+     */
+    public function isEnabled()
+    {
+        return $this->enabled;
+    }
+
+    /**
+     * @see PicoPluginInterface::isStatusChanged()
+     */
+    public function isStatusChanged()
+    {
+        return $this->statusChanged;
+    }
+
+    /**
+     * @see PicoPluginInterface::getPico()
+     */
+    public function getPico()
+    {
+        return $this->pico;
+    }
+
+    /**
+     * Passes all not satisfiable method calls to Pico
+     *
+     * @see    Pico
+     * @param  string $methodName name of the method to call
+     * @param  array  $params     parameters to pass
+     * @return mixed              return value of the called method
+     */
+    public function __call($methodName, array $params)
+    {
+        if (method_exists($this->getPico(), $methodName)) {
+            return call_user_func_array(array($this->getPico(), $methodName), $params);
+        }
+
+        throw new BadMethodCallException(
+            'Call to undefined method ' . get_class($this->getPico()) . '::' . $methodName . '() '
+            . 'through ' . get_called_class() . '::__call()'
+        );
+    }
+
+    /**
+     * Enables all plugins which this plugin depends on
+     *
+     * @see    PicoPluginInterface::getDependencies()
+     * @param  boolean $recursive enable required plugins automatically
+     * @return void
+     * @throws RuntimeException   thrown when a dependency fails
+     */
+    protected function checkDependencies($recursive)
+    {
+        foreach ($this->getDependencies() as $pluginName) {
+            try {
+                $plugin = $this->getPlugin($pluginName);
+            } catch (RuntimeException $e) {
+                throw new RuntimeException(
+                    "Unable to enable plugin '" . get_called_class() . "':"
+                    . "Required plugin '" . $pluginName . "' not found"
+                );
+            }
+
+            // plugins which don't implement PicoPluginInterface are always enabled
+            if (is_a($plugin, 'PicoPluginInterface') && !$plugin->isEnabled()) {
+                if ($recursive) {
+                    if (!$plugin->isStatusChanged()) {
+                        $plugin->setEnabled(true, true, true);
+                    } else {
+                        throw new RuntimeException(
+                            "Unable to enable plugin '" . get_called_class() . "':"
+                            . "Required plugin '" . $pluginName . "' was disabled manually"
+                        );
+                    }
+                } else {
+                    throw new RuntimeException(
+                        "Unable to enable plugin '" . get_called_class() . "':"
+                        . "Required plugin '" . $pluginName . "' is disabled"
+                    );
+                }
+            }
+        }
+    }
+
+    /**
+     * @see PicoPluginInterface::getDependencies()
+     */
+    public function getDependencies()
+    {
+        return (array) $this->dependsOn;
+    }
+
+    /**
+     * Disables all plugins which depend on this plugin
+     *
+     * @see    PicoPluginInterface::getDependants()
+     * @param  boolean $recursive disabled dependant plugins automatically
+     * @return void
+     * @throws RuntimeException   thrown when a dependency fails
+     */
+    protected function checkDependants($recursive)
+    {
+        $dependants = $this->getDependants();
+        if (!empty($dependants)) {
+            if ($recursive) {
+                foreach ($this->getDependants() as $pluginName => $plugin) {
+                    if ($plugin->isEnabled()) {
+                        if (!$plugin->isStatusChanged()) {
+                            $plugin->setEnabled(false, true, true);
+                        } else {
+                            throw new RuntimeException(
+                                "Unable to disable plugin '" . get_called_class() . "': "
+                                . "Required by manually enabled plugin '" . $pluginName . "'"
+                            );
+                        }
+                    }
+                }
+            } else {
+                $dependantsList = 'plugin' . ((count($dependants) > 1) ? 's' : '') . ' ';
+                $dependantsList .= "'" . implode("', '", array_keys($dependants)) . "'";
+                throw new RuntimeException(
+                    "Unable to disable plugin '" . get_called_class() . "': "
+                    . "Required by " . $dependantsList
+                );
+            }
+        }
+    }
+
+    /**
+     * @see PicoPluginInterface::getDependants()
+     */
+    public function getDependants()
+    {
+        if ($this->dependants === null) {
+            $this->dependants = array();
+            foreach ($this->getPlugins() as $pluginName => $plugin) {
+                // only plugins which implement PicoPluginInterface support dependencies
+                if (is_a($plugin, 'PicoPluginInterface')) {
+                    $dependencies = $plugin->getDependencies();
+                    if (in_array(get_called_class(), $dependencies)) {
+                        $this->dependants[$pluginName] = $plugin;
+                    }
+                }
+            }
+        }
+
+        return $this->dependants;
+    }
+}

+ 1276 - 0
lib/Pico.php

@@ -0,0 +1,1276 @@
+<?php
+
+/**
+ * Pico
+ *
+ * Pico is a stupidly simple, blazing fast, flat file CMS.
+ * - Stupidly Simple: Pico makes creating and maintaining a
+ *   website as simple as editing text files.
+ * - Blazing Fast: Pico is seriously lightweight and doesn't
+ *   use a database, making it super fast.
+ * - No Database: Pico is a "flat file" CMS, meaning no
+ *   database woes, no MySQL queries, nothing.
+ * - Markdown Formatting: Edit your website in your favourite
+ *   text editor using simple Markdown formatting.
+ * - Twig Templates: Pico uses the Twig templating engine,
+ *   for powerful and flexible themes.
+ * - Open Source: Pico is completely free and open source,
+ *   released under the MIT license.
+ * See <http://picocms.org/> for more info.
+ *
+ * @author  Gilbert Pellegrom
+ * @author  Daniel Rudolf
+ * @link    <http://picocms.org>
+ * @license The MIT License <http://opensource.org/licenses/MIT>
+ * @version 1.0
+ */
+class Pico
+{
+    /**
+     * Sort files in alphabetical ascending order
+     *
+     * @see Pico::getFiles()
+     * @var int
+     */
+    const SORT_ASC = 0;
+
+    /**
+     * Sort files in alphabetical descending order
+     *
+     * @see Pico::getFiles()
+     * @var int
+     */
+    const SORT_DESC = 1;
+
+    /**
+     * Don't sort files
+     *
+     * @see Pico::getFiles()
+     * @var int
+     */
+    const SORT_NONE = 2;
+
+    /**
+     * Root directory of this Pico instance
+     *
+     * @see Pico::getRootDir()
+     * @var string
+     */
+    protected $rootDir;
+
+    /**
+     * Config directory of this Pico instance
+     *
+     * @see Pico::getConfigDir()
+     * @var string
+     */
+    protected $configDir;
+
+    /**
+     * Plugins directory of this Pico instance
+     *
+     * @see Pico::getPluginsDir()
+     * @var string
+     */
+    protected $pluginsDir;
+
+    /**
+     * Themes directory of this Pico instance
+     *
+     * @see Pico::getThemesDir()
+     * @var string
+     */
+    protected $themesDir;
+
+    /**
+     * Boolean indicating whether Pico started processing yet
+     *
+     * @var boolean
+     */
+    protected $locked = false;
+
+    /**
+     * List of loaded plugins
+     *
+     * @see Pico::getPlugins()
+     * @var object[]|null
+     */
+    protected $plugins;
+
+    /**
+     * Current configuration of this Pico instance
+     *
+     * @see Pico::getConfig()
+     * @var mixed[]|null
+     */
+    protected $config;
+
+    /**
+     * Part of the URL describing the requested contents
+     *
+     * @see Pico::getRequestUrl()
+     * @var string|null
+     */
+    protected $requestUrl;
+
+    /**
+     * Absolute path to the content file being served
+     *
+     * @see Pico::getRequestFile()
+     * @var string|null
+     */
+    protected $requestFile;
+
+    /**
+     * Raw, not yet parsed contents to serve
+     *
+     * @see Pico::getRawContent()
+     * @var string|null
+     */
+    protected $rawContent;
+
+    /**
+     * Meta data of the page to serve
+     *
+     * @see Pico::getFileMeta()
+     * @var string[]|null
+     */
+    protected $meta;
+
+    /**
+     * Parsed content being served
+     *
+     * @see Pico::getFileContent()
+     * @var string|null
+     */
+    protected $content;
+
+    /**
+     * List of known pages
+     *
+     * @see Pico::getPages()
+     * @var array[]|null
+     */
+    protected $pages;
+
+    /**
+     * Data of the page being served
+     *
+     * @see Pico::getCurrentPage()
+     * @var array|null
+     */
+    protected $currentPage;
+
+    /**
+     * Data of the previous page relative to the page being served
+     *
+     * @see Pico::getPreviousPage()
+     * @var array|null
+     */
+    protected $previousPage;
+
+    /**
+     * Data of the next page relative to the page being served
+     *
+     * @see Pico::getNextPage()
+     * @var array|null
+     */
+    protected $nextPage;
+
+    /**
+     * Twig instance used for template parsing
+     *
+     * @see Pico::getTwig()
+     * @var Twig_Environment|null
+     */
+    protected $twig;
+
+    /**
+     * Variables passed to the twig template
+     *
+     * @see Pico::getTwigVariables
+     * @var mixed[]|null
+     */
+    protected $twigVariables;
+
+    /**
+     * Constructs a new Pico instance
+     *
+     * To carry out all the processing in Pico, call {@link Pico::run()}.
+     *
+     * @param string $rootDir    root directory of this Pico instance
+     * @param string $configDir  config directory of this Pico instance
+     * @param string $pluginsDir plugins directory of this Pico instance
+     * @param string $themesDir  themes directory of this Pico instance
+     */
+    public function __construct($rootDir, $configDir, $pluginsDir, $themesDir)
+    {
+        $this->rootDir = rtrim($rootDir, '/') . '/';
+        $this->configDir = $this->getAbsolutePath($configDir);
+        $this->pluginsDir = $this->getAbsolutePath($pluginsDir);
+        $this->themesDir = $this->getAbsolutePath($themesDir);
+    }
+
+    /**
+     * Returns the root directory of this Pico instance
+     *
+     * @return string root directory path
+     */
+    public function getRootDir()
+    {
+        return $this->rootDir;
+    }
+
+    /**
+     * Returns the config directory of this Pico instance
+     *
+     * @return string config directory path
+     */
+    public function getConfigDir()
+    {
+        return $this->configDir;
+    }
+
+    /**
+     * Returns the plugins directory of this Pico instance
+     *
+     * @return string plugins directory path
+     */
+    public function getPluginsDir()
+    {
+        return $this->pluginsDir;
+    }
+
+    /**
+     * Returns the themes directory of this Pico instance
+     *
+     * @return string themes directory path
+     */
+    public function getThemesDir()
+    {
+        return $this->themesDir;
+    }
+
+    /**
+     * Runs this Pico instance
+     *
+     * Loads plugins, evaluates the config file, does URL routing, parses
+     * meta headers, processes Markdown, does Twig processing and returns
+     * the rendered contents.
+     *
+     * @return string rendered Pico contents
+     */
+    public function run()
+    {
+        // lock Pico
+        $this->locked = true;
+
+        // load plugins
+        $this->loadPlugins();
+        $this->triggerEvent('onPluginsLoaded', array(&$this->plugins));
+
+        // load config
+        $this->loadConfig();
+        $this->triggerEvent('onConfigLoaded', array(&$this->config));
+
+        // evaluate request url
+        $this->evaluateRequestUrl();
+        $this->triggerEvent('onRequestUrl', array(&$this->requestUrl));
+
+        // discover requested file
+        $this->discoverRequestFile();
+        $this->triggerEvent('onRequestFile', array(&$this->requestFile));
+
+        // load raw file content
+        $this->triggerEvent('onContentLoading', array(&$this->requestFile));
+
+        if (file_exists($this->requestFile)) {
+            $this->rawContent = $this->loadFileContent($this->requestFile);
+        } else {
+            $this->triggerEvent('on404ContentLoading', array(&$this->requestFile));
+
+            header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
+            $this->rawContent = $this->load404Content($this->requestFile);
+
+            $this->triggerEvent('on404ContentLoaded', array(&$this->rawContent));
+        }
+
+        $this->triggerEvent('onContentLoaded', array(&$this->rawContent));
+
+        // parse file meta
+        $headers = $this->getMetaHeaders();
+
+        $this->triggerEvent('onMetaParsing', array(&$this->rawContent, &$headers));
+        $this->meta = $this->parseFileMeta($this->rawContent, $headers);
+        $this->triggerEvent('onMetaParsed', array(&$this->meta));
+
+        // parse file content
+        $this->triggerEvent('onContentParsing', array(&$this->rawContent));
+
+        $this->content = $this->prepareFileContent($this->rawContent, $this->meta);
+        $this->triggerEvent('onContentPrepared', array(&$this->content));
+
+        $this->content = $this->parseFileContent($this->content);
+        $this->triggerEvent('onContentParsed', array(&$this->content));
+
+        // read pages
+        $this->triggerEvent('onPagesLoading');
+
+        $this->readPages();
+        $this->sortPages();
+        $this->discoverCurrentPage();
+
+        $this->triggerEvent('onPagesLoaded', array(
+            &$this->pages,
+            &$this->currentPage,
+            &$this->previousPage,
+            &$this->nextPage
+        ));
+
+        // register twig
+        $this->triggerEvent('onTwigRegistration');
+        $this->registerTwig();
+
+        // render template
+        $this->twigVariables = $this->getTwigVariables();
+        if (isset($this->meta['template']) && $this->meta['template']) {
+            $templateName = $this->meta['template'];
+        } else {
+            $templateName = 'index';
+        }
+        if (file_exists($this->getThemesDir() . $this->getConfig('theme') . '/' . $templateName . '.twig')) {
+            $templateName .= '.twig';
+        } else {
+            $templateName .= '.html';
+        }
+
+        $this->triggerEvent('onPageRendering', array(&$this->twig, &$this->twigVariables, &$templateName));
+
+        $output = $this->twig->render($templateName, $this->twigVariables);
+        $this->triggerEvent('onPageRendered', array(&$output));
+
+        return $output;
+    }
+
+    /**
+     * Loads plugins from Pico::$pluginsDir in alphabetical order
+     *
+     * Plugin files may be prefixed by a number (e.g. 00-PicoDeprecated.php)
+     * to indicate their processing order. You MUST NOT use prefixes between
+     * 00 and 19 (reserved for built-in plugins).
+     *
+     * @see    Pico::getPlugin()
+     * @see    Pico::getPlugins()
+     * @return void
+     * @throws RuntimeException thrown when a plugin couldn't be loaded
+     */
+    protected function loadPlugins()
+    {
+        $this->plugins = array();
+        $pluginFiles = $this->getFiles($this->getPluginsDir(), '.php');
+        foreach ($pluginFiles as $pluginFile) {
+            require_once($pluginFile);
+
+            $className = preg_replace('/^[0-9]+-/', '', basename($pluginFile, '.php'));
+            if (class_exists($className)) {
+                // class name and file name can differ regarding case sensitivity
+                $plugin = new $className($this);
+                $className = get_class($plugin);
+
+                $this->plugins[$className] = $plugin;
+            } else {
+                // TODO: breaks backward compatibility
+                //throw new RuntimeException("Unable to load plugin '".$className."'");
+            }
+        }
+    }
+
+    /**
+     * Returns the instance of a named plugin
+     *
+     * Plugins SHOULD implement {@link PicoPluginInterface}, but you MUST NOT
+     * rely on it. For more information see {@link PicoPluginInterface}.
+     *
+     * @see    Pico::loadPlugins()
+     * @see    Pico::getPlugins()
+     * @param  string           $pluginName name of the plugin
+     * @return object                       instance of the plugin
+     * @throws RuntimeException             thrown when the plugin wasn't found
+     */
+    public function getPlugin($pluginName)
+    {
+        if (isset($this->plugins[$pluginName])) {
+            return $this->plugins[$pluginName];
+        }
+
+        throw new RuntimeException("Missing plugin '" . $pluginName . "'");
+    }
+
+    /**
+     * Returns all loaded plugins
+     *
+     * @see    Pico::loadPlugins()
+     * @see    Pico::getPlugin()
+     * @return object[]|null
+     */
+    public function getPlugins()
+    {
+        return $this->plugins;
+    }
+
+    /**
+     * Loads the config.php from Pico::$configDir
+     *
+     * @see    Pico::setConfig()
+     * @see    Pico::getConfig()
+     * @return void
+     */
+    protected function loadConfig()
+    {
+        $config = null;
+        $defaultConfig = array(
+            'site_title' => 'Pico',
+            'base_url' => '',
+            'rewrite_url' => null,
+            'theme' => 'default',
+            'date_format' => '%D %T',
+            'twig_config' => array('cache' => false, 'autoescape' => false, 'debug' => false),
+            'pages_order_by' => 'alpha',
+            'pages_order' => 'asc',
+            'content_dir' => null,
+            'content_ext' => '.md',
+            'timezone' => ''
+        );
+
+        $configFile = $this->getConfigDir() . 'config.php';
+        if (file_exists($configFile)) {
+            require $configFile;
+        }
+
+        $this->config = is_array($this->config) ? $this->config : array();
+        $this->config += is_array($config) ? $config + $defaultConfig : $defaultConfig;
+
+        if (empty($this->config['base_url'])) {
+            $this->config['base_url'] = $this->getBaseUrl();
+        } else {
+            $this->config['base_url'] = rtrim($this->config['base_url'], '/') . '/';
+        }
+
+        if (empty($this->config['content_dir'])) {
+            // try to guess the content directory
+            if (is_dir($this->getRootDir() . 'content')) {
+                $this->config['content_dir'] = $this->getRootDir() . 'content/';
+            } else {
+                $this->config['content_dir'] = $this->getRootDir() . 'content-sample/';
+            }
+        } else {
+            $this->config['content_dir'] = $this->getAbsolutePath($this->config['content_dir']);
+        }
+
+        if (empty($this->config['timezone'])) {
+            // explicitly set a default timezone to prevent a E_NOTICE
+            // when no timezone is set; the `date_default_timezone_get()`
+            // function always returns a timezone, at least UTC
+            $this->config['timezone'] = date_default_timezone_get();
+        }
+        date_default_timezone_set($this->config['timezone']);
+    }
+
+    /**
+     * Sets Pico's config before calling Pico::run()
+     *
+     * This method allows you to modify Pico's config without creating a
+     * {@path "config/config.php"} or changing some of its variables before
+     * Pico starts processing.
+     *
+     * You can call this method between {@link Pico::__construct()} and
+     * {@link Pico::run()} only. Options set with this method cannot be
+     * overwritten by {@path "config/config.php"}.
+     *
+     * @see    Pico::loadConfig()
+     * @see    Pico::getConfig()
+     * @param  mixed[] $config  array with config variables
+     * @return void
+     * @throws RuntimeException thrown if Pico already started processing
+     */
+    public function setConfig(array $config)
+    {
+        if ($this->locked) {
+            throw new RuntimeException("You cannot modify Pico's config after processing has started");
+        }
+
+        $this->config = $config;
+    }
+
+    /**
+     * Returns either the value of the specified config variable or
+     * the config array
+     *
+     * @see    Pico::setConfig()
+     * @see    Pico::loadConfig()
+     * @param  string $configName optional name of a config variable
+     * @return mixed              returns either the value of the named config
+     *     variable, null if the config variable doesn't exist or the config
+     *     array if no config name was supplied
+     */
+    public function getConfig($configName = null)
+    {
+        if ($configName !== null) {
+            return isset($this->config[$configName]) ? $this->config[$configName] : null;
+        } else {
+            return $this->config;
+        }
+    }
+
+    /**
+     * Evaluates the requested URL
+     *
+     * Pico 1.0 uses the `QUERY_STRING` routing method (e.g. `/pico/?sub/page`)
+     * to support SEO-like URLs out-of-the-box with any webserver. You can
+     * still setup URL rewriting (e.g. using `mod_rewrite` on Apache) to
+     * basically remove the `?` from URLs, but your rewritten URLs must follow
+     * the new `QUERY_STRING` principles. URL rewriting requires some special
+     * configuration on your webserver, but this should be "basic work" for
+     * any webmaster...
+     *
+     * Pico 0.9 and older required Apache with `mod_rewrite` enabled, thus old
+     * plugins, templates and contents may require you to enable URL rewriting
+     * to work. If you're upgrading from Pico 0.9, you will probably have to
+     * update your rewriting rules.
+     *
+     * We recommend you to use the `link` filter in templates to create
+     * internal links, e.g. `{{ "sub/page"|link }}` is equivalent to
+     * `{{ base_url }}sub/page`. In content files you can still use the
+     * `%base_url%` variable; e.g. `%base_url%?sub/page` will be automatically
+     * replaced accordingly.
+     *
+     * @see    Pico::getRequestUrl()
+     * @return void
+     */
+    protected function evaluateRequestUrl()
+    {
+        // use QUERY_STRING; e.g. /pico/?sub/page
+        // if you want to use rewriting, you MUST make your rules to
+        // rewrite the URLs to follow the QUERY_STRING method
+        //
+        // Note: you MUST NOT call the index page with /pico/?someBooleanParameter;
+        // use /pico/?someBooleanParameter= or /pico/?index&someBooleanParameter instead
+        $pathComponent = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
+        if (($pathComponentLength = strpos($pathComponent, '&')) !== false) {
+            $pathComponent = substr($pathComponent, 0, $pathComponentLength);
+        }
+        $this->requestUrl = (strpos($pathComponent, '=') === false) ? urldecode($pathComponent) : '';
+    }
+
+    /**
+     * Returns the URL where a user requested the page
+     *
+     * @see    Pico::evaluateRequestUrl()
+     * @return string|null request URL
+     */
+    public function getRequestUrl()
+    {
+        return $this->requestUrl;
+    }
+
+    /**
+     * Uses the request URL to discover the content file to serve
+     *
+     * @see    Pico::getRequestFile()
+     * @return void
+     */
+    protected function discoverRequestFile()
+    {
+        if (empty($this->requestUrl)) {
+            $this->requestFile = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext');
+        } else {
+            // prevent content_dir breakouts using malicious request URLs
+            // we don't use realpath() here because we neither want to check for file existance
+            // nor prohibit symlinks which intentionally point to somewhere outside the content_dir
+            // it is STRONGLY RECOMMENDED to use open_basedir - always, not just with Pico!
+            $requestUrl = str_replace('\\', '/', $this->requestUrl);
+            $requestUrlParts = explode('/', $requestUrl);
+
+            $requestFileParts = array();
+            foreach ($requestUrlParts as $requestUrlPart) {
+                if (($requestUrlPart === '') || ($requestUrlPart === '.')) {
+                    continue;
+                } elseif ($requestUrlPart === '..') {
+                    array_pop($requestFileParts);
+                    continue;
+                }
+
+                $requestFileParts[] = $requestUrlPart;
+            }
+
+            if (empty($requestFileParts)) {
+                $this->requestFile = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext');
+                return;
+            }
+
+            // discover the content file to serve
+            // Note: $requestFileParts neither contains a trailing nor a leading slash
+            $this->requestFile = $this->getConfig('content_dir') . implode('/', $requestFileParts);
+            if (is_dir($this->requestFile)) {
+                // if no index file is found, try a accordingly named file in the previous dir
+                // if this file doesn't exist either, show the 404 page, but assume the index
+                // file as being requested (maintains backward compatibility to Pico < 1.0)
+                $indexFile = $this->requestFile . '/index' . $this->getConfig('content_ext');
+                if (file_exists($indexFile) || !file_exists($this->requestFile . $this->getConfig('content_ext'))) {
+                    $this->requestFile = $indexFile;
+                    return;
+                }
+            }
+            $this->requestFile .= $this->getConfig('content_ext');
+        }
+    }
+
+    /**
+     * Returns the absolute path to the content file to serve
+     *
+     * @see    Pico::discoverRequestFile()
+     * @return string|null file path
+     */
+    public function getRequestFile()
+    {
+        return $this->requestFile;
+    }
+
+    /**
+     * Returns the raw contents of a file
+     *
+     * @see    Pico::getRawContent()
+     * @param  string $file file path
+     * @return string       raw contents of the file
+     */
+    public function loadFileContent($file)
+    {
+        return file_get_contents($file);
+    }
+
+    /**
+     * Returns the raw contents of the first found 404 file when traversing
+     * up from the directory the requested file is in
+     *
+     * @see    Pico::getRawContent()
+     * @param  string $file     path to requested (but not existing) file
+     * @return string           raw contents of the 404 file
+     * @throws RuntimeException thrown when no suitable 404 file is found
+     */
+    public function load404Content($file)
+    {
+        $errorFileDir = substr($file, strlen($this->getConfig('content_dir')));
+        do {
+            $errorFileDir = dirname($errorFileDir);
+            $errorFile = $errorFileDir . '/404' . $this->getConfig('content_ext');
+        } while (!file_exists($this->getConfig('content_dir') . $errorFile) && ($errorFileDir !== '.'));
+
+        if (!file_exists($this->getConfig('content_dir') . $errorFile)) {
+            $errorFile = ($errorFileDir === '.') ? '404' . $this->getConfig('content_ext') : $errorFile;
+            throw new RuntimeException('Required "' . $errorFile . '" not found');
+        }
+
+        return $this->loadFileContent($this->getConfig('content_dir') . $errorFile);
+    }
+
+    /**
+     * Returns the raw contents, either of the requested or the 404 file
+     *
+     * @see    Pico::loadFileContent()
+     * @see    Pico::load404Content()
+     * @return string|null raw contents
+     */
+    public function getRawContent()
+    {
+        return $this->rawContent;
+    }
+
+    /**
+     * Returns known meta headers and triggers the onMetaHeaders event
+     *
+     * Heads up! Calling this method triggers the `onMetaHeaders` event.
+     * Keep this in mind to prevent a infinite loop!
+     *
+     * @return string[] known meta headers; the array value specifies the
+     *     YAML key to search for, the array key is later used to access the
+     *     found value
+     */
+    public function getMetaHeaders()
+    {
+        $headers = array(
+            'title' => 'Title',
+            'description' => 'Description',
+            'author' => 'Author',
+            'date' => 'Date',
+            'robots' => 'Robots',
+            'template' => 'Template'
+        );
+
+        $this->triggerEvent('onMetaHeaders', array(&$headers));
+        return $headers;
+    }
+
+    /**
+     * Parses the file meta from raw file contents
+     *
+     * Meta data MUST start on the first line of the file, either opened and
+     * closed by `---` or C-style block comments (deprecated). The headers are
+     * parsed by the YAML component of the Symfony project, keys are lowered.
+     * If you're a plugin developer, you MUST register new headers during the
+     * `onMetaHeaders` event first. The implicit availability of headers is
+     * for users and pure (!) theme developers ONLY.
+     *
+     * @see    Pico::getFileMeta()
+     * @see    <http://symfony.com/doc/current/components/yaml/introduction.html>
+     * @param  string   $rawContent the raw file contents
+     * @param  string[] $headers    known meta headers
+     * @return array                parsed meta data
+     */
+    public function parseFileMeta($rawContent, array $headers)
+    {
+        $meta = array();
+        $pattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n"
+            . "(.*?)(?:\r)?\n(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s";
+        if (preg_match($pattern, $rawContent, $rawMetaMatches)) {
+            $yamlParser = new \Symfony\Component\Yaml\Parser();
+            $meta = $yamlParser->parse($rawMetaMatches[3]);
+            $meta = array_change_key_case($meta, CASE_LOWER);
+
+            foreach ($headers as $fieldId => $fieldName) {
+                $fieldName = strtolower($fieldName);
+                if (isset($meta[$fieldName])) {
+                    // rename field (e.g. remove whitespaces)
+                    if ($fieldId != $fieldName) {
+                        $meta[$fieldId] = $meta[$fieldName];
+                        unset($meta[$fieldName]);
+                    }
+                } else {
+                    // guarantee array key existance
+                    $meta[$fieldId] = '';
+                }
+            }
+
+            if (!empty($meta['date'])) {
+                $meta['time'] = strtotime($meta['date']);
+                $meta['date_formatted'] = utf8_encode(strftime($this->getConfig('date_format'), $meta['time']));
+            } else {
+                $meta['time'] = $meta['date_formatted'] = '';
+            }
+        } else {
+            // guarantee array key existance
+            foreach ($headers as $id => $field) {
+                $meta[$id] = '';
+            }
+
+            $meta['time'] = $meta['date_formatted'] = '';
+        }
+
+        return $meta;
+    }
+
+    /**
+     * Returns the parsed meta data of the requested page
+     *
+     * @see    Pico::parseFileMeta()
+     * @return array|null parsed meta data
+     */
+    public function getFileMeta()
+    {
+        return $this->meta;
+    }
+
+    /**
+     * Applies some static preparations to the raw contents of a page,
+     * e.g. removing the meta header and replacing %base_url%
+     *
+     * @see    Pico::parseFileContent()
+     * @see    Pico::getFileContent()
+     * @param  string $rawContent raw contents of a page
+     * @param  array  $meta       meta data to use for %meta.*% replacement
+     * @return string             contents prepared for parsing
+     */
+    public function prepareFileContent($rawContent, array $meta)
+    {
+        // remove meta header
+        $metaHeaderPattern = "/^(\/(\*)|---)[[:blank:]]*(?:\r)?\n"
+            . "(.*?)(?:\r)?\n(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s";
+        $content = preg_replace($metaHeaderPattern, '', $rawContent, 1);
+
+        // replace %site_title%
+        $content = str_replace('%site_title%', $this->getConfig('site_title'), $content);
+
+        // replace %base_url%
+        if ($this->isUrlRewritingEnabled()) {
+            // always use `%base_url%?sub/page` syntax for internal links
+            // we'll replace the links accordingly, depending on enabled rewriting
+            $content = str_replace('%base_url%?', $this->getBaseUrl(), $content);
+        } else {
+            // actually not necessary, but makes the URL look a little nicer
+            $content = str_replace('%base_url%?', $this->getBaseUrl() . '?', $content);
+        }
+        $content = str_replace('%base_url%', rtrim($this->getBaseUrl(), '/'), $content);
+
+        // replace %theme_url%
+        $themeUrl = $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme');
+        $content = str_replace('%theme_url%', $themeUrl, $content);
+
+        // replace %meta.*%
+        if (!empty($meta)) {
+            $metaKeys = $metaValues = array();
+            foreach ($meta as $metaKey => $metaValue) {
+                if (is_scalar($metaValue) || ($metaValue === null)) {
+                    $metaKeys[] = '%meta.' . $metaKey . '%';
+                    $metaValues[] = strval($metaValue);
+                }
+            }
+            $content = str_replace($metaKeys, $metaValues, $content);
+        }
+
+        return $content;
+    }
+
+    /**
+     * Parses the contents of a page using ParsedownExtra
+     *
+     * @see    Pico::prepareFileContent()
+     * @see    Pico::getFileContent()
+     * @param  string $content raw contents of a page (Markdown)
+     * @return string          parsed contents (HTML)
+     */
+    public function parseFileContent($content)
+    {
+        $parsedown = new ParsedownExtra();
+        return $parsedown->text($content);
+    }
+
+    /**
+     * Returns the cached contents of the requested page
+     *
+     * @see    Pico::prepareFileContent()
+     * @see    Pico::parseFileContent()
+     * @return string|null parsed contents
+     */
+    public function getFileContent()
+    {
+        return $this->content;
+    }
+
+    /**
+     * Reads the data of all pages known to Pico
+     *
+     * The page data will be an array containing the following values:
+     * <pre>
+     * +----------------+--------+------------------------------------------+
+     * | Array key      | Type   | Description                              |
+     * +----------------+--------+------------------------------------------+
+     * | id             | string | relative path to the content file        |
+     * | url            | string | URL to the page                          |
+     * | title          | string | title of the page (YAML header)          |
+     * | description    | string | description of the page (YAML header)    |
+     * | author         | string | author of the page (YAML header)         |
+     * | time           | string | timestamp derived from the Date header   |
+     * | date           | string | date of the page (YAML header)           |
+     * | date_formatted | string | formatted date of the page               |
+     * | raw_content    | string | raw, not yet parsed contents of the page |
+     * | meta           | string | parsed meta data of the page             |
+     * +----------------+--------+------------------------------------------+
+     * </pre>
+     *
+     * @see    Pico::sortPages()
+     * @see    Pico::getPages()
+     * @return void
+     */
+    protected function readPages()
+    {
+        $this->pages = array();
+        $files = $this->getFiles($this->getConfig('content_dir'), $this->getConfig('content_ext'), Pico::SORT_NONE);
+        foreach ($files as $i => $file) {
+            // skip 404 page
+            if (basename($file) == '404' . $this->getConfig('content_ext')) {
+                unset($files[$i]);
+                continue;
+            }
+
+            $id = substr($file, strlen($this->getConfig('content_dir')), -strlen($this->getConfig('content_ext')));
+
+            // drop inaccessible pages (e.g. drop "sub.md" if "sub/index.md" exists)
+            $conflictFile = $this->getConfig('content_dir') . $id . '/index' . $this->getConfig('content_ext');
+            if (in_array($conflictFile, $files, true)) {
+                continue;
+            }
+
+            $url = $this->getPageUrl($id);
+            if ($file != $this->requestFile) {
+                $rawContent = file_get_contents($file);
+                $meta = $this->parseFileMeta($rawContent, $this->getMetaHeaders());
+            } else {
+                $rawContent = &$this->rawContent;
+                $meta = &$this->meta;
+            }
+
+            // build page data
+            // title, description, author and date are assumed to be pretty basic data
+            // everything else is accessible through $page['meta']
+            $page = array(
+                'id' => $id,
+                'url' => $url,
+                'title' => &$meta['title'],
+                'description' => &$meta['description'],
+                'author' => &$meta['author'],
+                'time' => &$meta['time'],
+                'date' => &$meta['date'],
+                'date_formatted' => &$meta['date_formatted'],
+                'raw_content' => &$rawContent,
+                'meta' => &$meta
+            );
+
+            if ($file == $this->requestFile) {
+                $page['content'] = &$this->content;
+            }
+
+            unset($rawContent, $meta);
+
+            // trigger event
+            $this->triggerEvent('onSinglePageLoaded', array(&$page));
+
+            $this->pages[$id] = $page;
+        }
+    }
+
+    /**
+     * Sorts all pages known to Pico
+     *
+     * @see    Pico::readPages()
+     * @see    Pico::getPages()
+     * @return void
+     */
+    protected function sortPages()
+    {
+        // sort pages
+        $order = $this->getConfig('pages_order');
+        $alphaSortClosure = function ($a, $b) use ($order) {
+            $aSortKey = (basename($a['id']) === 'index') ? dirname($a['id']) : $a['id'];
+            $bSortKey = (basename($b['id']) === 'index') ? dirname($b['id']) : $b['id'];
+
+            $cmp = strcmp($aSortKey, $bSortKey);
+            return $cmp * (($order == 'desc') ? -1 : 1);
+        };
+
+        if ($this->getConfig('pages_order_by') == 'date') {
+            // sort by date
+            uasort($this->pages, function ($a, $b) use ($alphaSortClosure, $order) {
+                if (empty($a['time']) || empty($b['time'])) {
+                    $cmp = (empty($a['time']) - empty($b['time']));
+                } else {
+                    $cmp = ($b['time'] - $a['time']);
+                }
+
+                if ($cmp === 0) {
+                    // never assume equality; fallback to alphabetical order
+                    return $alphaSortClosure($a, $b);
+                }
+
+                return $cmp * (($order == 'desc') ? 1 : -1);
+            });
+        } else {
+            // sort alphabetically
+            uasort($this->pages, $alphaSortClosure);
+        }
+    }
+
+    /**
+     * Returns the list of known pages
+     *
+     * @see    Pico::readPages()
+     * @see    Pico::sortPages()
+     * @return array|null the data of all pages
+     */
+    public function getPages()
+    {
+        return $this->pages;
+    }
+
+    /**
+     * Walks through the list of known pages and discovers the requested page
+     * as well as the previous and next page relative to it
+     *
+     * @see    Pico::getCurrentPage()
+     * @see    Pico::getPreviousPage()
+     * @see    Pico::getNextPage()
+     * @return void
+     */
+    protected function discoverCurrentPage()
+    {
+        $pageIds = array_keys($this->pages);
+
+        $contentDir = $this->getConfig('content_dir');
+        $contentExt = $this->getConfig('content_ext');
+        $currentPageId = substr($this->requestFile, strlen($contentDir), -strlen($contentExt));
+        $currentPageIndex = array_search($currentPageId, $pageIds);
+        if ($currentPageIndex !== false) {
+            $this->currentPage = &$this->pages[$currentPageId];
+
+            if (($this->getConfig('order_by') == 'date') && ($this->getConfig('order') == 'desc')) {
+                $previousPageOffset = 1;
+                $nextPageOffset = -1;
+            } else {
+                $previousPageOffset = -1;
+                $nextPageOffset = 1;
+            }
+
+            if (isset($pageIds[$currentPageIndex + $previousPageOffset])) {
+                $previousPageId = $pageIds[$currentPageIndex + $previousPageOffset];
+                $this->previousPage = &$this->pages[$previousPageId];
+            }
+
+            if (isset($pageIds[$currentPageIndex + $nextPageOffset])) {
+                $nextPageId = $pageIds[$currentPageIndex + $nextPageOffset];
+                $this->nextPage = &$this->pages[$nextPageId];
+            }
+        }
+    }
+
+    /**
+     * Returns the data of the requested page
+     *
+     * @see    Pico::discoverCurrentPage()
+     * @return array|null page data
+     */
+    public function getCurrentPage()
+    {
+        return $this->currentPage;
+    }
+
+    /**
+     * Returns the data of the previous page relative to the page being served
+     *
+     * @see    Pico::discoverCurrentPage()
+     * @return array|null page data
+     */
+    public function getPreviousPage()
+    {
+        return $this->previousPage;
+    }
+
+    /**
+     * Returns the data of the next page relative to the page being served
+     *
+     * @see    Pico::discoverCurrentPage()
+     * @return array|null page data
+     */
+    public function getNextPage()
+    {
+        return $this->nextPage;
+    }
+
+    /**
+     * Registers the twig template engine
+     *
+     * @see    Pico::getTwig()
+     * @return void
+     */
+    protected function registerTwig()
+    {
+        $twigLoader = new Twig_Loader_Filesystem($this->getThemesDir() . $this->getConfig('theme'));
+        $this->twig = new Twig_Environment($twigLoader, $this->getConfig('twig_config'));
+        $this->twig->addExtension(new Twig_Extension_Debug());
+
+        // register link filter
+        $this->twig->addFilter(new Twig_SimpleFilter('link', array($this, 'getPageUrl')));
+
+        // register content filter
+        $pico = $this;
+        $pages = &$this->pages;
+        $this->twig->addFilter(new Twig_SimpleFilter('content', function ($pageId) use ($pico, &$pages) {
+            if (isset($pages[$pageId])) {
+                $pageData = &$pages[$pageId];
+                if (!isset($pageData['content'])) {
+                    $pageData['content'] = $pico->prepareFileContent($pageData['raw_content'], $pageData['meta']);
+                    $pageData['content'] = $pico->parseFileContent($pageData['content']);
+                }
+                return $pageData['content'];
+            }
+            return '';
+        }));
+    }
+
+    /**
+     * Returns the twig template engine
+     *
+     * @see    Pico::registerTwig()
+     * @return Twig_Environment|null twig template engine
+     */
+    public function getTwig()
+    {
+        return $this->twig;
+    }
+
+    /**
+     * Returns the variables passed to the template
+     *
+     * URLs and paths (namely `base_dir`, `base_url`, `theme_dir` and
+     * `theme_url`) don't add a trailing slash for historic reasons.
+     *
+     * @return mixed[] template variables
+     */
+    protected function getTwigVariables()
+    {
+        $frontPage = $this->getConfig('content_dir') . 'index' . $this->getConfig('content_ext');
+        return array(
+            'config' => $this->getConfig(),
+            'base_dir' => rtrim($this->getRootDir(), '/'),
+            'base_url' => rtrim($this->getBaseUrl(), '/'),
+            'theme_dir' => $this->getThemesDir() . $this->getConfig('theme'),
+            'theme_url' => $this->getBaseUrl() . basename($this->getThemesDir()) . '/' . $this->getConfig('theme'),
+            'rewrite_url' => $this->isUrlRewritingEnabled(),
+            'site_title' => $this->getConfig('site_title'),
+            'meta' => $this->meta,
+            'content' => $this->content,
+            'pages' => $this->pages,
+            'prev_page' => $this->previousPage,
+            'current_page' => $this->currentPage,
+            'next_page' => $this->nextPage,
+            'is_front_page' => ($this->requestFile == $frontPage),
+        );
+    }
+
+    /**
+     * Returns the base URL of this Pico instance
+     *
+     * @return string the base url
+     */
+    public function getBaseUrl()
+    {
+        $baseUrl = $this->getConfig('base_url');
+        if (!empty($baseUrl)) {
+            return $baseUrl;
+        }
+
+        if (
+            (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
+            || ($_SERVER['SERVER_PORT'] == 443)
+            || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
+        ) {
+            $protocol = 'https';
+        } else {
+            $protocol = 'http';
+        }
+
+        $this->config['base_url'] =
+            $protocol . "://" . $_SERVER['HTTP_HOST']
+            . dirname($_SERVER['SCRIPT_NAME']) . '/';
+
+        return $this->getConfig('base_url');
+    }
+
+    /**
+     * Returns true if URL rewriting is enabled
+     *
+     * @return boolean true if URL rewriting is enabled, false otherwise
+     */
+    public function isUrlRewritingEnabled()
+    {
+        if (($this->getConfig('rewrite_url') === null) && isset($_SERVER['PICO_URL_REWRITING'])) {
+            return (bool) $_SERVER['PICO_URL_REWRITING'];
+        } elseif ($this->getConfig('rewrite_url')) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the URL to a given page
+     *
+     * @param  string $page identifier of the page to link to
+     * @return string       URL
+     */
+    public function getPageUrl($page)
+    {
+        return $this->getBaseUrl() . ((!$this->isUrlRewritingEnabled() && !empty($page)) ? '?' : '') . $page;
+    }
+
+    /**
+     * Recursively walks through a directory and returns all containing files
+     * matching the specified file extension
+     *
+     * @param  string $directory     start directory
+     * @param  string $fileExtension return files with the given file extension
+     *     only (optional)
+     * @param  int    $order         specify whether and how files should be
+     *     sorted; use Pico::SORT_ASC for a alphabetical ascending order (this
+     *     is the default behaviour), Pico::SORT_DESC for a descending order
+     *     or Pico::SORT_NONE to leave the result unsorted
+     * @return array                 list of found files
+     */
+    protected function getFiles($directory, $fileExtension = '', $order = self::SORT_ASC)
+    {
+        $directory = rtrim($directory, '/');
+        $result = array();
+
+        // scandir() reads files in alphabetical order
+        $files = scandir($directory, $order);
+        $fileExtensionLength = strlen($fileExtension);
+        if ($files !== false) {
+            foreach ($files as $file) {
+                // exclude hidden files/dirs starting with a .; this also excludes the special dirs . and ..
+                // exclude files ending with a ~ (vim/nano backup) or # (emacs backup)
+                if ((substr($file, 0, 1) === '.') || in_array(substr($file, -1), array('~', '#'))) {
+                    continue;
+                }
+
+                if (is_dir($directory . '/' . $file)) {
+                    // get files recursively
+                    $result = array_merge($result, $this->getFiles($directory . '/' . $file, $fileExtension, $order));
+                } elseif (empty($fileExtension) || (substr($file, -$fileExtensionLength) === $fileExtension)) {
+                    $result[] = $directory . '/' . $file;
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Makes a relative path absolute to Pico's root dir
+     *
+     * This method also guarantees a trailing slash.
+     *
+     * @param  string $path relative or absolute path
+     * @return string       absolute path
+     */
+    protected function getAbsolutePath($path)
+    {
+        if (substr($path, 0, 1) !== '/') {
+            $path = $this->getRootDir() . $path;
+        }
+        return rtrim($path, '/') . '/';
+    }
+
+    /**
+     * Triggers events on plugins which implement PicoPluginInterface
+     *
+     * Deprecated events (as used by plugins not implementing
+     * {@link IPocPlugin}) are triggered by {@link PicoDeprecated}.
+     *
+     * @see    PicoPluginInterface
+     * @see    AbstractPicoPlugin
+     * @see    DummyPlugin
+     * @param  string $eventName name of the event to trigger
+     * @param  array  $params    optional parameters to pass
+     * @return void
+     */
+    protected function triggerEvent($eventName, array $params = array())
+    {
+        if (!empty($this->plugins)) {
+            foreach ($this->plugins as $plugin) {
+                // only trigger events for plugins that implement PicoPluginInterface
+                // deprecated events (plugins for Pico 0.9 and older) will be
+                // triggered by the `PicoPluginDeprecated` plugin
+                if (is_a($plugin, 'PicoPluginInterface')) {
+                    $plugin->handleEvent($eventName, $params);
+                }
+            }
+        }
+    }
+}

+ 102 - 0
lib/PicoPluginInterface.php

@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * Common interface for Pico plugins
+ *
+ * For a list of supported events see {@link DummyPlugin}; you can use
+ * {@link DummyPlugin} as template for new plugins. For a list of deprecated
+ * events see {@link PicoDeprecated}.
+ *
+ * You SHOULD NOT use deprecated events when implementing this interface.
+ * Deprecated events are triggered by the {@link PicoDeprecated} plugin, if
+ * plugins which don't implement this interface are loaded. You can take
+ * advantage from this behaviour if you want to do something only when old
+ * plugins are loaded. Consequently the old events are never triggered when
+ * your plugin is implementing this interface and no old plugins are present.
+ *
+ * If you're developing a new plugin, you MUST implement this interface. If
+ * you're the developer of an old plugin, it is STRONGLY RECOMMENDED to use
+ * the events introduced in Pico 1.0 when releasing a new version of your
+ * plugin. If you want to use any of the new events, you MUST implement
+ * this interface and update all other events you use.
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+interface PicoPluginInterface
+{
+    /**
+     * Constructs a new instance of a Pico plugin
+     *
+     * @param Pico $pico current instance of Pico
+     */
+    public function __construct(Pico $pico);
+
+    /**
+     * Handles a event that was triggered by Pico
+     *
+     * @param  string $eventName name of the triggered event
+     * @param  array  $params    passed parameters
+     * @return void
+     */
+    public function handleEvent($eventName, array $params);
+
+    /**
+     * Enables or disables this plugin
+     *
+     * @see    PicoPluginInterface::isEnabled()
+     * @see    PicoPluginInterface::isStatusChanged()
+     * @param  boolean $enabled     enable (true) or disable (false) this plugin
+     * @param  boolean $recursive   when true, enable or disable recursively
+     *     In other words, if you enable a plugin, all required plugins are
+     *     enabled, too. When disabling a plugin, all depending plugins are
+     *     disabled likewise. Recursive operations are only performed as long
+     *     as a plugin wasn't enabled/disabled manually. This parameter is
+     *     optional and defaults to true.
+     * @param  boolean $auto        enable or disable to fulfill a dependency
+     *     This parameter is optional and defaults to false.
+     * @return void
+     * @throws RuntimeException     thrown when a dependency fails
+     */
+    public function setEnabled($enabled, $recursive = true, $auto = false);
+
+    /**
+     * Returns true if this plugin is enabled, false otherwise
+     *
+     * @see    PicoPluginInterface::setEnabled()
+     * @return boolean plugin is enabled (true) or disabled (false)
+     */
+    public function isEnabled();
+
+    /**
+     * Returns true if the plugin was ever enabled/disabled manually
+     *
+     * @see    PicoPluginInterface::setEnabled()
+     * @return boolean plugin is in its default state (true), false otherwise
+     */
+    public function isStatusChanged();
+
+    /**
+     * Returns a list of names of plugins required by this plugin
+     *
+     * @return string[] required plugins
+     */
+    public function getDependencies();
+
+    /**
+     * Returns a list of plugins which depend on this plugin
+     *
+     * @return object[] dependant plugins
+     */
+    public function getDependants();
+
+    /**
+     * Returns the plugins instance of Pico
+     *
+     * @see    Pico
+     * @return Pico the plugins instance of Pico
+     */
+    public function getPico();
+}

+ 0 - 2
lib/cache/.gitignore

@@ -1,2 +0,0 @@
-*
-!.gitignore

+ 0 - 0
lib/index.html


+ 0 - 405
lib/pico.php

@@ -1,405 +0,0 @@
-<?php
-
-/**
- * Pico
- *
- * @author Gilbert Pellegrom
- * @link http://picocms.org
- * @license http://opensource.org/licenses/MIT
- * @version 0.8
- */
-class Pico
-{
-
-    private $config;
-    private $plugins;
-
-    /**
-     * The constructor carries out all the processing in Pico.
-     * Does URL routing, Markdown processing and Twig processing.
-     */
-    public function __construct()
-    {
-        // Load plugins
-        $this->load_plugins();
-        $this->run_hooks('plugins_loaded');
-
-        // Load the settings
-        $settings = $this->get_config();
-        $this->run_hooks('config_loaded', array(&$settings));
-
-        // Get request url and script url
-        $url = '';
-        $request_url = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
-        $script_url = (isset($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : '';
-
-        // Get our url path and trim the / of the left and the right
-        if ($request_url != $script_url) {
-            $url = trim(preg_replace('/' . str_replace('/', '\/', str_replace('index.php', '', $script_url)) . '/', '',
-                $request_url, 1), '/');
-        }
-        $url = preg_replace('/\?.*/', '', $url); // Strip query string
-        $this->run_hooks('request_url', array(&$url));
-
-        // Get the file path
-        if ($url) {
-            $file = $settings['content_dir'] . $url;
-        } else {
-            $file = $settings['content_dir'] . 'index';
-        }
-
-        // Load the file
-        if (is_dir($file)) {
-            $file = $settings['content_dir'] . $url . '/index' . CONTENT_EXT;
-        } else {
-            $file .= CONTENT_EXT;
-        }
-
-        $this->run_hooks('before_load_content', array(&$file));
-        if (file_exists($file)) {
-            $content = file_get_contents($file);
-        } else {
-            $this->run_hooks('before_404_load_content', array(&$file));
-            $content = file_get_contents($settings['content_dir'] . '404' . CONTENT_EXT);
-            header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
-            $this->run_hooks('after_404_load_content', array(&$file, &$content));
-        }
-        $this->run_hooks('after_load_content', array(&$file, &$content));
-
-        $meta = $this->read_file_meta($content);
-        $this->run_hooks('file_meta', array(&$meta));
-
-        $this->run_hooks('before_parse_content', array(&$content));
-        $content = $this->parse_content($content);
-        $this->run_hooks('after_parse_content', array(&$content));
-        $this->run_hooks('content_parsed', array(&$content)); // Depreciated @ v0.8
-
-        // Get all the pages
-        $pages = $this->get_pages($settings['base_url'], $settings['pages_order_by'], $settings['pages_order'],
-            $settings['excerpt_length']);
-        $prev_page = array();
-        $current_page = array();
-        $next_page = array();
-        while ($current_page = current($pages)) {
-            if ((isset($meta['title'])) && ($meta['title'] == $current_page['title'])) {
-                break;
-            }
-            next($pages);
-        }
-        $prev_page = next($pages);
-        prev($pages);
-        $next_page = prev($pages);
-        $this->run_hooks('get_pages', array(&$pages, &$current_page, &$prev_page, &$next_page));
-
-        // Load the theme
-        $this->run_hooks('before_twig_register');
-        Twig_Autoloader::register();
-        $loader = new Twig_Loader_Filesystem(THEMES_DIR . $settings['theme']);
-        $twig = new Twig_Environment($loader, $settings['twig_config']);
-        $twig->addExtension(new Twig_Extension_Debug());
-        $twig_vars = array(
-            'config' => $settings,
-            'base_dir' => rtrim(ROOT_DIR, '/'),
-            'base_url' => $settings['base_url'],
-            'theme_dir' => THEMES_DIR . $settings['theme'],
-            'theme_url' => $settings['base_url'] . '/' . basename(THEMES_DIR) . '/' . $settings['theme'],
-            'site_title' => $settings['site_title'],
-            'meta' => $meta,
-            'content' => $content,
-            'pages' => $pages,
-            'prev_page' => $prev_page,
-            'current_page' => $current_page,
-            'next_page' => $next_page,
-            'is_front_page' => $url ? false : true,
-        );
-
-        $template = (isset($meta['template']) && $meta['template']) ? $meta['template'] : 'index';
-        $this->run_hooks('before_render', array(&$twig_vars, &$twig, &$template));
-        $output = $twig->render($template . '.html', $twig_vars);
-        $this->run_hooks('after_render', array(&$output));
-        echo $output;
-    }
-
-    /**
-     * Load any plugins
-     */
-    protected function load_plugins()
-    {
-        $this->plugins = array();
-        $plugins = $this->get_files(PLUGINS_DIR, '.php');
-        if (!empty($plugins)) {
-            foreach ($plugins as $plugin) {
-                include_once($plugin);
-                $plugin_name = preg_replace("/\\.[^.\\s]{3}$/", '', basename($plugin));
-                if (class_exists($plugin_name)) {
-                    $obj = new $plugin_name;
-                    $this->plugins[] = $obj;
-                }
-            }
-        }
-    }
-
-    /**
-     * Parses the content using Parsedown-extra
-     *
-     * @param string $content the raw txt content
-     * @return string $content the Markdown formatted content
-     */
-    protected function parse_content($content)
-    {
-        $content = preg_replace('#/\*.+?\*/#s', '', $content, 1); // Remove first comment (with meta)
-        $content = str_replace('%base_url%', $this->base_url(), $content);
-        $Parsedown = new ParsedownExtra();
-		$content= $Parsedown->text($content);
-
-        return $content;
-    }
-
-    /**
-     * Parses the file meta from the txt file header
-     *
-     * @param string $content the raw txt content
-     * @return array $headers an array of meta values
-     */
-    protected function read_file_meta($content)
-    {
-        $config = $this->config;
-
-        $headers = array(
-            'title' => 'Title',
-            'description' => 'Description',
-            'author' => 'Author',
-            'date' => 'Date',
-            'robots' => 'Robots',
-            'template' => 'Template'
-        );
-
-        // Add support for custom headers by hooking into the headers array
-        $this->run_hooks('before_read_file_meta', array(&$headers));
-
-        foreach ($headers as $field => $regex) {
-            if (preg_match('/^[ \t\/*#@]*' . preg_quote($regex, '/') . ':(.*)$/mi', $content, $match) && $match[1]) {
-                $headers[$field] = trim(preg_replace("/\s*(?:\*\/|\?>).*/", '', $match[1]));
-            } else {
-                $headers[$field] = '';
-            }
-        }
-
-        if (isset($headers['date'])) {
-            $headers['date_formatted'] = utf8_encode(strftime($config['date_format'], strtotime($headers['date'])));
-        }
-
-        return $headers;
-    }
-
-    /**
-     * Loads the config
-     *
-     * @return array $config an array of config values
-     */
-    protected function get_config()
-    {
-        if (file_exists(CONFIG_DIR . 'config.php')) {
-            $this->config = require(CONFIG_DIR . 'config.php');
-        } else if (file_exists(ROOT_DIR . 'config.php')) {
-            // deprecated
-            $this->config = require(ROOT_DIR . 'config.php');
-        }
-
-        $defaults = array(
-            'site_title' => 'Pico',
-            'base_url' => $this->base_url(),
-            'theme' => 'default',
-            'date_format' => '%D %T',
-            'twig_config' => array('cache' => false, 'autoescape' => false, 'debug' => false),
-            'pages_order_by' => 'alpha',
-            'pages_order' => 'asc',
-            'excerpt_length' => 50,
-            'content_dir' => 'content-sample/',
-        );
-
-        if (is_array($this->config)) {
-            $this->config = array_merge($defaults, $this->config);
-        } else {
-            $this->config = $defaults;
-        }
-
-        return $this->config;
-    }
-
-    /**
-     * Get a list of pages
-     *
-     * @param string $base_url the base URL of the site
-     * @param string $order_by order by "alpha" or "date"
-     * @param string $order order "asc" or "desc"
-     * @return array $sorted_pages an array of pages
-     */
-    protected function get_pages($base_url, $order_by = 'alpha', $order = 'asc', $excerpt_length = 50)
-    {
-        $config = $this->config;
-
-        $pages = $this->get_files($config['content_dir'], CONTENT_EXT);
-        $sorted_pages = array();
-        $date_id = 0;
-        foreach ($pages as $key => $page) {
-            // Skip 404
-            if (basename($page) == '404' . CONTENT_EXT) {
-                unset($pages[$key]);
-                continue;
-            }
-
-            // Ignore Emacs (and Nano) temp files
-            if (in_array(substr($page, -1), array('~', '#'))) {
-                unset($pages[$key]);
-                continue;
-            }
-            // Get title and format $page
-            $page_content = file_get_contents($page);
-            $page_meta = $this->read_file_meta($page_content);
-            $page_content = $this->parse_content($page_content);
-            $url = str_replace($config['content_dir'], $base_url . '/', $page);
-            $url = str_replace('index' . CONTENT_EXT, '', $url);
-            $url = str_replace(CONTENT_EXT, '', $url);
-            $data = array(
-                'title' => isset($page_meta['title']) ? $page_meta['title'] : '',
-                'url' => $url,
-                'author' => isset($page_meta['author']) ? $page_meta['author'] : '',
-                'date' => isset($page_meta['date']) ? $page_meta['date'] : '',
-                'date_formatted' => isset($page_meta['date']) ? utf8_encode(strftime($config['date_format'],
-                    strtotime($page_meta['date']))) : '',
-                'content' => $page_content,
-                'excerpt' => $this->limit_words(strip_tags($page_content), $excerpt_length),
-                //this addition allows the 'description' meta to be picked up in content areas... specifically to replace 'excerpt'
-                'description' => isset($page_meta['description']) ? $page_meta['description'] : '',
-
-            );
-
-            // Extend the data provided with each page by hooking into the data array
-            $this->run_hooks('get_page_data', array(&$data, $page_meta));
-
-            if ($order_by == 'date' && isset($page_meta['date'])) {
-                $sorted_pages[$page_meta['date'] . $date_id] = $data;
-                $date_id++;
-            } else {
-                $sorted_pages[$page] = $data;
-            }
-        }
-
-        if ($order == 'desc') {
-            krsort($sorted_pages);
-        } else {
-            ksort($sorted_pages);
-        }
-
-        return $sorted_pages;
-    }
-
-    /**
-     * Processes any hooks and runs them
-     *
-     * @param string $hook_id the ID of the hook
-     * @param array $args optional arguments
-     */
-    protected function run_hooks($hook_id, $args = array())
-    {
-        if (!empty($this->plugins)) {
-            foreach ($this->plugins as $plugin) {
-                if (is_callable(array($plugin, $hook_id))) {
-                    call_user_func_array(array($plugin, $hook_id), $args);
-                }
-            }
-        }
-    }
-
-    /**
-     * Helper function to work out the base URL
-     *
-     * @return string the base url
-     */
-    protected function base_url()
-    {
-        $config = $this->config;
-
-        if (isset($config['base_url']) && $config['base_url']) {
-            return $config['base_url'];
-        }
-
-        $url = '';
-        $request_url = (isset($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : '';
-        $script_url = (isset($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : '';
-        if ($request_url != $script_url) {
-            $url = trim(preg_replace('/' . str_replace('/', '\/', str_replace('index.php', '', $script_url)) . '/', '',
-                $request_url, 1), '/');
-        }
-
-        $protocol = $this->get_protocol();
-
-        return rtrim(str_replace($url, '', $protocol . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']), '/');
-    }
-
-    /**
-     * Tries to guess the server protocol. Used in base_url()
-     *
-     * @return string the current protocol
-     */
-    protected function get_protocol()
-    {
-        $protocol = 'http';
-        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off' && $_SERVER['HTTPS'] != '') {
-            $protocol = 'https';
-        }
-
-        return $protocol;
-    }
-
-    /**
-     * Helper function to recusively get all files in a directory
-     *
-     * @param string $directory start directory
-     * @param string $ext optional limit to file extensions
-     * @return array the matched files
-     */
-    protected function get_files($directory, $ext = '')
-    {
-        $array_items = array();
-        if ($files = scandir($directory)) {
-            foreach ($files as $file) {
-                if (in_array(substr($file, -1), array('~', '#'))) {
-                    continue;
-                }
-                if (preg_match("/^(^\.)/", $file) === 0) {
-                    if (is_dir($directory . "/" . $file)) {
-                        $array_items = array_merge($array_items, $this->get_files($directory . "/" . $file, $ext));
-                    } else {
-                        $file = $directory . "/" . $file;
-                        if (!$ext || strstr($file, $ext)) {
-                            $array_items[] = preg_replace("/\/\//si", "/", $file);
-                        }
-                    }
-                }
-            }
-        }
-
-        return $array_items;
-    }
-
-    /**
-     * Helper function to limit the words in a string
-     *
-     * @param string $string the given string
-     * @param int $word_limit the number of words to limit to
-     * @return string the limited string
-     */
-    protected function limit_words($string, $word_limit)
-    {
-        $words = explode(' ', $string);
-        $excerpt = trim(implode(' ', array_splice($words, 0, $word_limit)));
-        if (count($words) > $word_limit) {
-            $excerpt .= '&hellip;';
-        }
-
-        return $excerpt;
-    }
-
-}

+ 430 - 0
plugins/00-PicoDeprecated.php

@@ -0,0 +1,430 @@
+<?php
+
+/**
+ * Serve features of Pico deprecated since v1.0
+ *
+ * This plugin exists for backward compatibility and is disabled by default.
+ * It gets automatically enabled when a plugin which doesn't implement
+ * {@link PicoPluginInterface} is loaded. This plugin triggers deprecated
+ * events and automatically enables {@link PicoParsePagesContent} and
+ * {@link PicoExcerpt}. These plugins heavily impact Pico's performance! You
+ * can disable this plugin by calling {@link PicoDeprecated::setEnabled()}.
+ *
+ * The following deprecated events are triggered by this plugin:
+ * <pre>
+ * +---------------------+-----------------------------------------------------------+
+ * | Event               | ... triggers the deprecated event                         |
+ * +---------------------+-----------------------------------------------------------+
+ * | onPluginsLoaded     | plugins_loaded()                                          |
+ * | onConfigLoaded      | config_loaded($config)                                    |
+ * | onRequestUrl        | request_url($url)                                         |
+ * | onContentLoading    | before_load_content($file)                                |
+ * | onContentLoaded     | after_load_content($file, $rawContent)                    |
+ * | on404ContentLoading | before_404_load_content($file)                            |
+ * | on404ContentLoaded  | after_404_load_content($file, $rawContent)                |
+ * | onMetaHeaders       | before_read_file_meta($headers)                           |
+ * | onMetaParsed        | file_meta($meta)                                          |
+ * | onContentParsing    | before_parse_content($rawContent)                         |
+ * | onContentParsed     | after_parse_content($content)                             |
+ * | onContentParsed     | content_parsed($content)                                  |
+ * | onSinglePageLoaded  | get_page_data($pages, $meta)                              |
+ * | onPagesLoaded       | get_pages($pages, $currentPage, $previousPage, $nextPage) |
+ * | onTwigRegistration  | before_twig_register()                                    |
+ * | onPageRendering     | before_render($twigVariables, $twig, $templateName)       |
+ * | onPageRendered      | after_render($output)                                     |
+ * +---------------------+-----------------------------------------------------------+
+ * </pre>
+ *
+ * Since Pico 1.0 the config is stored in {@path "config/config.php"}. This
+ * plugin tries to read {@path "config.php"} in Pico's root dir and overwrites
+ * all settings previously specified in {@path "config/config.php"}.
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+class PicoDeprecated extends AbstractPicoPlugin
+{
+    /**
+     * This plugin is disabled by default
+     *
+     * @see AbstractPicoPlugin::$enabled
+     */
+    protected $enabled = false;
+
+    /**
+     * The requested file
+     *
+     * @see PicoDeprecated::getRequestFile()
+     * @var string|null
+     */
+    protected $requestFile;
+
+    /**
+     * Enables this plugin on demand and triggers the deprecated event
+     * plugins_loaded()
+     *
+     * @see DummyPlugin::onPluginsLoaded()
+     */
+    public function onPluginsLoaded(&$plugins)
+    {
+        if (!empty($plugins)) {
+            foreach ($plugins as $plugin) {
+                if (!is_a($plugin, 'PicoPluginInterface')) {
+                    // the plugin doesn't implement PicoPluginInterface; it uses deprecated events
+                    // enable PicoDeprecated if it hasn't be explicitly enabled/disabled yet
+                    if (!$this->isStatusChanged()) {
+                        $this->setEnabled(true, true, true);
+                    }
+                    break;
+                }
+            }
+        } else {
+            // no plugins were found, so it actually isn't necessary to call deprecated events
+            // anyway, this plugin also ensures compatibility apart from events used by old plugins,
+            // so enable PicoDeprecated if it hasn't be explicitly enabled/disabled yet
+            if (!$this->isStatusChanged()) {
+                $this->setEnabled(true, true, true);
+            }
+        }
+
+        if ($this->isEnabled()) {
+            $this->triggerEvent('plugins_loaded');
+        }
+    }
+
+    /**
+     * Triggers the deprecated event config_loaded($config)
+     *
+     * This method also defines deprecated constants, reads the `config.php`
+     * in Pico's root dir, enables the plugins {@link PicoParsePagesContent}
+     * and {@link PicoExcerpt} and makes `$config` globally accessible (the
+     * latter was removed with Pico 0.9 and was added again as deprecated
+     * feature with Pico 1.0)
+     *
+     * @see    PicoDeprecated::defineConstants()
+     * @see    PicoDeprecated::loadRootDirConfig()
+     * @see    PicoDeprecated::enablePlugins()
+     * @see    DummyPlugin::onConfigLoaded()
+     * @param  mixed[] &$realConfig array of config variables
+     * @return void
+     */
+    public function onConfigLoaded(&$realConfig)
+    {
+        global $config;
+
+        $this->defineConstants();
+        $this->loadRootDirConfig($realConfig);
+        $this->enablePlugins();
+        $config = &$realConfig;
+
+        $this->triggerEvent('config_loaded', array(&$realConfig));
+    }
+
+    /**
+     * Defines deprecated constants
+     *
+     * `ROOT_DIR`, `LIB_DIR`, `PLUGINS_DIR`, `THEMES_DIR` and `CONTENT_EXT`
+     * are deprecated since v1.0, `CONTENT_DIR` existed just in v0.9,
+     * `CONFIG_DIR` just for a short time between v0.9 and v1.0 and
+     * `CACHE_DIR` was dropped with v1.0 without a replacement.
+     *
+     * @see    PicoDeprecated::onConfigLoaded()
+     * @return void
+     */
+    protected function defineConstants()
+    {
+        if (!defined('ROOT_DIR')) {
+            define('ROOT_DIR', $this->getRootDir());
+        }
+        if (!defined('CONFIG_DIR')) {
+            define('CONFIG_DIR', $this->getConfigDir());
+        }
+        if (!defined('LIB_DIR')) {
+            $picoReflector = new ReflectionClass('Pico');
+            define('LIB_DIR', dirname($picoReflector->getFileName() . '/'));
+        }
+        if (!defined('PLUGINS_DIR')) {
+            define('PLUGINS_DIR', $this->getPluginsDir());
+        }
+        if (!defined('THEMES_DIR')) {
+            define('THEMES_DIR', $this->getThemesDir());
+        }
+        if (!defined('CONTENT_DIR')) {
+            define('CONTENT_DIR', $this->getConfig('content_dir'));
+        }
+        if (!defined('CONTENT_EXT')) {
+            define('CONTENT_EXT', $this->getConfig('content_ext'));
+        }
+    }
+
+    /**
+     * Read config.php in Pico's root dir
+     *
+     * @see    PicoDeprecated::onConfigLoaded()
+     * @see    Pico::loadConfig()
+     * @param  mixed[] &$realConfig array of config variables
+     * @return void
+     */
+    protected function loadRootDirConfig(&$realConfig)
+    {
+        if (file_exists($this->getRootDir() . 'config.php')) {
+            // config.php in Pico::$rootDir is deprecated; use Pico::$configDir instead
+            $config = null;
+            require($this->getRootDir() . 'config.php');
+
+            if (is_array($config)) {
+                $realConfig = $config + $realConfig;
+            }
+        }
+    }
+
+    /**
+     * Enables the plugins PicoParsePagesContent and PicoExcerpt
+     *
+     * @see    PicoParsePagesContent
+     * @see    PicoExcerpt
+     * @return void
+     */
+    protected function enablePlugins()
+    {
+        // enable PicoParsePagesContent and PicoExcerpt
+        // we can't enable them during onPluginsLoaded because we can't know
+        // if the user disabled us (PicoDeprecated) manually in the config
+        $plugins = $this->getPlugins();
+        if (isset($plugins['PicoParsePagesContent'])) {
+            // parse all pages content if this plugin hasn't
+            // be explicitly enabled/disabled yet
+            if (!$plugins['PicoParsePagesContent']->isStatusChanged()) {
+                $plugins['PicoParsePagesContent']->setEnabled(true, true, true);
+            }
+        }
+        if (isset($plugins['PicoExcerpt'])) {
+            // enable excerpt plugin if it hasn't be explicitly enabled/disabled yet
+            if (!$plugins['PicoExcerpt']->isStatusChanged()) {
+                $plugins['PicoExcerpt']->setEnabled(true, true, true);
+            }
+        }
+    }
+
+    /**
+     * Triggers the deprecated event request_url($url)
+     *
+     * @see DummyPlugin::onRequestUrl()
+     */
+    public function onRequestUrl(&$url)
+    {
+        $this->triggerEvent('request_url', array(&$url));
+    }
+
+    /**
+     * Sets PicoDeprecated::$requestFile to trigger the deprecated
+     * events after_load_content() and after_404_load_content()
+     *
+     * @see PicoDeprecated::onContentLoaded()
+     * @see PicoDeprecated::on404ContentLoaded()
+     * @see DummyPlugin::onRequestFile()
+     */
+    public function onRequestFile(&$file)
+    {
+        $this->requestFile = &$file;
+    }
+
+    /**
+     * Triggers the deprecated before_load_content($file)
+     *
+     * @see DummyPlugin::onContentLoading()
+     */
+    public function onContentLoading(&$file)
+    {
+        $this->triggerEvent('before_load_content', array(&$file));
+    }
+
+    /**
+     * Triggers the deprecated event after_load_content($file, $rawContent)
+     *
+     * @see DummyPlugin::onContentLoaded()
+     */
+    public function onContentLoaded(&$rawContent)
+    {
+        $this->triggerEvent('after_load_content', array(&$this->requestFile, &$rawContent));
+    }
+
+    /**
+     * Triggers the deprecated before_404_load_content($file)
+     *
+     * @see DummyPlugin::on404ContentLoading()
+     */
+    public function on404ContentLoading(&$file)
+    {
+        $this->triggerEvent('before_404_load_content', array(&$file));
+    }
+
+    /**
+     * Triggers the deprecated event after_404_load_content($file, $rawContent)
+     *
+     * @see DummyPlugin::on404ContentLoaded()
+     */
+    public function on404ContentLoaded(&$rawContent)
+    {
+        $this->triggerEvent('after_404_load_content', array(&$this->requestFile, &$rawContent));
+    }
+
+    /**
+     * Triggers the deprecated event before_read_file_meta($headers)
+     *
+     * @see DummyPlugin::onMetaHeaders()
+     */
+    public function onMetaHeaders(&$headers)
+    {
+        $this->triggerEvent('before_read_file_meta', array(&$headers));
+    }
+
+    /**
+     * Triggers the deprecated event file_meta($meta)
+     *
+     * @see DummyPlugin::onMetaParsed()
+     */
+    public function onMetaParsed(&$meta)
+    {
+        $this->triggerEvent('file_meta', array(&$meta));
+    }
+
+    /**
+     * Triggers the deprecated event before_parse_content($rawContent)
+     *
+     * @see DummyPlugin::onContentParsing()
+     */
+    public function onContentParsing(&$rawContent)
+    {
+        $this->triggerEvent('before_parse_content', array(&$rawContent));
+    }
+
+    /**
+     * Triggers the deprecated events after_parse_content($content) and
+     * content_parsed($content)
+     *
+     * @see DummyPlugin::onContentParsed()
+     */
+    public function onContentParsed(&$content)
+    {
+        $this->triggerEvent('after_parse_content', array(&$content));
+
+        // deprecated since v0.8
+        $this->triggerEvent('content_parsed', array(&$content));
+    }
+
+    /**
+     * Triggers the deprecated event get_page_data($pages, $meta)
+     *
+     * @see DummyPlugin::onSinglePageLoaded()
+     */
+    public function onSinglePageLoaded(&$pageData)
+    {
+        $this->triggerEvent('get_page_data', array(&$pageData, $pageData['meta']));
+    }
+
+    /**
+     * Triggers the deprecated event
+     * get_pages($pages, $currentPage, $previousPage, $nextPage)
+     *
+     * Please note that the `get_pages()` event gets `$pages` passed without a
+     * array index. The index is rebuild later using either the `id` array key
+     * or is derived from the `url` array key. Duplicates are prevented by
+     * adding `~dup` when necessary.
+     *
+     * @see DummyPlugin::onPagesLoaded()
+     */
+    public function onPagesLoaded(&$pages, &$currentPage, &$previousPage, &$nextPage)
+    {
+        // remove keys of pages array
+        $plainPages = array();
+        foreach ($pages as &$pageData) {
+            $plainPages[] = &$pageData;
+        }
+        unset($pageData);
+
+        $this->triggerEvent('get_pages', array(&$plainPages, &$currentPage, &$previousPage, &$nextPage));
+
+        // re-index pages array
+        $pages = array();
+        foreach ($plainPages as &$pageData) {
+            if (!isset($pageData['id'])) {
+                $urlPrefixLength = strlen($this->getBaseUrl()) + intval(!$this->isUrlRewritingEnabled());
+                $pageData['id'] = substr($pageData['url'], $urlPrefixLength);
+            }
+
+            // prevent duplicates
+            $id = $pageData['id'];
+            for ($i = 1; isset($pages[$id]); $i++) {
+                $id = $pageData['id'] . '~dup' . $i;
+            }
+
+            $pages[$id] = &$pageData;
+        }
+    }
+
+    /**
+     * Triggers the deprecated event before_twig_register()
+     *
+     * @see DummyPlugin::onTwigRegistration()
+     */
+    public function onTwigRegistration()
+    {
+        $this->triggerEvent('before_twig_register');
+    }
+
+    /**
+     * Triggers the deprecated event before_render($twigVariables, $twig, $templateName)
+     *
+     * Please note that the `before_render()` event gets `$templateName` passed
+     * without its file extension. The file extension is later added again.
+     *
+     * @see DummyPlugin::onPageRendering()
+     */
+    public function onPageRendering(&$twig, &$twigVariables, &$templateName)
+    {
+        // template name contains file extension since Pico 1.0
+        $fileExtension = '';
+        if (($fileExtensionPos = strrpos($templateName, '.')) !== false) {
+            $fileExtension = substr($templateName, $fileExtensionPos);
+            $templateName = substr($templateName, 0, $fileExtensionPos);
+        }
+
+        $this->triggerEvent('before_render', array(&$twigVariables, &$twig, &$templateName));
+
+        // add original file extension
+        $templateName = $templateName . $fileExtension;
+    }
+
+    /**
+     * Triggers the deprecated event after_render($output)
+     *
+     * @see DummyPlugin::onPageRendered()
+     */
+    public function onPageRendered(&$output)
+    {
+        $this->triggerEvent('after_render', array(&$output));
+    }
+
+    /**
+     * Triggers a deprecated event on all plugins
+     *
+     * Deprecated events are also triggered on plugins which implement
+     * {@link PicoPluginInterface}. Please note that the methods are called
+     * directly and not through {@link PicoPluginInterface::handleEvent()}.
+     *
+     * @param  string $eventName event to trigger
+     * @param  array  $params    parameters to pass
+     * @return void
+     */
+    protected function triggerEvent($eventName, array $params = array())
+    {
+        foreach ($this->getPlugins() as $plugin) {
+            if (method_exists($plugin, $eventName)) {
+                call_user_func_array(array($plugin, $eventName), $params);
+            }
+        }
+    }
+}

+ 40 - 0
plugins/01-PicoParsePagesContent.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Parses the contents of all pages
+ *
+ * This plugin exists for backward compatibility and is disabled by default.
+ * It gets automatically enabled when {@link PicoDeprecated} is enabled. You
+ * can avoid this by calling {@link PicoParsePagesContent::setEnabled()}.
+ *
+ * This plugin heavily impacts Pico's performance, you should avoid to enable
+ * it whenever possible! If you must parse the contents of a page, do this
+ * selectively and only for pages you really need to.
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+class PicoParsePagesContent extends AbstractPicoPlugin
+{
+    /**
+     * This plugin is disabled by default
+     *
+     * @see AbstractPicoPlugin::$enabled
+     */
+    protected $enabled = false;
+
+    /**
+     * Parses the contents of all pages
+     *
+     * @see DummyPlugin::onSinglePageLoaded()
+     */
+    public function onSinglePageLoaded(&$pageData)
+    {
+        if (!isset($pageData['content'])) {
+            $pageData['content'] = $this->prepareFileContent($pageData['raw_content'], $pageData['meta']);
+            $pageData['content'] = $this->parseFileContent($pageData['content']);
+        }
+    }
+}

+ 81 - 0
plugins/02-PicoExcerpt.php

@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * Creates a excerpt for the contents of each page (as of Pico v0.9 and older)
+ *
+ * This plugin exists for backward compatibility and is disabled by default.
+ * It gets automatically enabled when {@link PicoDeprecated} is enabled. You
+ * can avoid this by calling {@link PicoExcerpt::setEnabled()}.
+ *
+ * This plugin doesn't do its job very well and depends on
+ * {@link PicoParsePagesContent}, what heavily impacts Pico's performance. You
+ * should either use the Description meta header field or write something own.
+ * Best solution seems to be a filter for twig, see e.g.
+ * {@link https://gist.github.com/james2doyle/6629712}.
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+class PicoExcerpt extends AbstractPicoPlugin
+{
+    /**
+     * This plugin is disabled by default
+     *
+     * @see AbstractPicoPlugin::$enabled
+     */
+    protected $enabled = false;
+
+    /**
+     * This plugin depends on PicoParsePagesContent
+     *
+     * @see PicoParsePagesContent
+     * @see AbstractPicoPlugin::$dependsOn
+     */
+    protected $dependsOn = array('PicoParsePagesContent');
+
+    /**
+     * Adds the default excerpt length of 50 words to the config
+     *
+     * @see DummyPlugin::onConfigLoaded()
+     */
+    public function onConfigLoaded(&$config)
+    {
+        if (!isset($config['excerpt_length'])) {
+            $config['excerpt_length'] = 50;
+        }
+    }
+
+    /**
+     * Creates a excerpt for the contents of each page
+     *
+     * @see PicoExcerpt::createExcerpt()
+     * @see DummyPlugin::onSinglePageLoaded()
+     */
+    public function onSinglePageLoaded(&$pageData)
+    {
+        if (!isset($pageData['excerpt'])) {
+            $pageData['excerpt'] = $this->createExcerpt(
+                strip_tags($pageData['content']),
+                $this->getConfig('excerpt_length')
+            );
+        }
+    }
+
+    /**
+     * Helper function to create a excerpt of a string
+     *
+     * @param  string $string    the string to create a excerpt from
+     * @param  int    $wordLimit the maximum number of words the excerpt should be long
+     * @return string            excerpt of $string
+     */
+    protected function createExcerpt($string, $wordLimit)
+    {
+        $words = explode(' ', $string);
+        if (count($words) > $wordLimit) {
+            return trim(implode(' ', array_slice($words, 0, $wordLimit))) . '&hellip;';
+        }
+        return $string;
+    }
+}

+ 313 - 0
plugins/DummyPlugin.php

@@ -0,0 +1,313 @@
+<?php
+
+/**
+ * Pico dummy plugin - a template for plugins
+ *
+ * You're a plugin developer? This template may be helpful :-)
+ * Simply remove the events you don't need and add your own logic.
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+class DummyPlugin extends AbstractPicoPlugin
+{
+    /**
+     * This plugin is enabled by default?
+     *
+     * @see AbstractPicoPlugin::$enabled
+     * @var boolean
+     */
+    protected $enabled = false;
+
+    /**
+     * This plugin depends on ...
+     *
+     * @see AbstractPicoPlugin::$dependsOn
+     * @var string[]
+     */
+    protected $dependsOn = array();
+
+    /**
+     * Triggered after Pico has loaded all available plugins
+     *
+     * This event is triggered nevertheless the plugin is enabled or not.
+     * It is NOT guaranteed that plugin dependencies are fulfilled!
+     *
+     * @see    Pico::getPlugin()
+     * @see    Pico::getPlugins()
+     * @param  object[] &$plugins loaded plugin instances
+     * @return void
+     */
+    public function onPluginsLoaded(&$plugins)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has read its configuration
+     *
+     * @see    Pico::getConfig()
+     * @param  mixed[] &$config array of config variables
+     * @return void
+     */
+    public function onConfigLoaded(&$config)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has evaluated the request URL
+     *
+     * @see    Pico::getRequestUrl()
+     * @param  string &$url part of the URL describing the requested contents
+     * @return void
+     */
+    public function onRequestUrl(&$url)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has discovered the content file to serve
+     *
+     * @see    Pico::getBaseUrl()
+     * @see    Pico::getRequestFile()
+     * @param  string &$file absolute path to the content file to serve
+     * @return void
+     */
+    public function onRequestFile(&$file)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico reads the contents of the file to serve
+     *
+     * @see    Pico::loadFileContent()
+     * @see    DummyPlugin::onContentLoaded()
+     * @param  string &$file path to the file which contents will be read
+     * @return void
+     */
+    public function onContentLoading(&$file)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has read the contents of the file to serve
+     *
+     * @see    Pico::getRawContent()
+     * @param  string &$rawContent raw file contents
+     * @return void
+     */
+    public function onContentLoaded(&$rawContent)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico reads the contents of a 404 file
+     *
+     * @see    Pico::load404Content()
+     * @see    DummyPlugin::on404ContentLoaded()
+     * @param  string &$file path to the file which contents were requested
+     * @return void
+     */
+    public function on404ContentLoading(&$file)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has read the contents of the 404 file
+     *
+     * @see    Pico::getRawContent()
+     * @param  string &$rawContent raw file contents
+     * @return void
+     */
+    public function on404ContentLoaded(&$rawContent)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered when Pico reads its known meta header fields
+     *
+     * @see    Pico::getMetaHeaders()
+     * @param  string[] &$headers list of known meta header
+     *     fields; the array value specifies the YAML key to search for, the
+     *     array key is later used to access the found value
+     * @return void
+     */
+    public function onMetaHeaders(&$headers)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico parses the meta header
+     *
+     * @see    Pico::parseFileMeta()
+     * @see    DummyPlugin::onMetaParsed()
+     * @param  string   &$rawContent raw file contents
+     * @param  string[] &$headers    known meta header fields
+     * @return void
+     */
+    public function onMetaParsing(&$rawContent, &$headers)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has parsed the meta header
+     *
+     * @see    Pico::getFileMeta()
+     * @param  string[] &$meta parsed meta data
+     * @return void
+     */
+    public function onMetaParsed(&$meta)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico parses the pages content
+     *
+     * @see    Pico::prepareFileContent()
+     * @see    DummyPlugin::prepareFileContent()
+     * @see    DummyPlugin::onContentParsed()
+     * @param  string &$rawContent raw file contents
+     * @return void
+     */
+    public function onContentParsing(&$rawContent)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has prepared the raw file contents for parsing
+     *
+     * @see    Pico::parseFileContent()
+     * @see    DummyPlugin::onContentParsed()
+     * @param  string &$content prepared file contents for parsing
+     * @return void
+     */
+    public function onContentPrepared(&$content)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has parsed the contents of the file to serve
+     *
+     * @see    Pico::getFileContent()
+     * @param  string &$content parsed contents
+     * @return void
+     */
+    public function onContentParsed(&$content)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico reads all known pages
+     *
+     * @see    Pico::readPages()
+     * @see    DummyPlugin::onSinglePageLoaded()
+     * @see    DummyPlugin::onPagesLoaded()
+     * @return void
+     */
+    public function onPagesLoading()
+    {
+        // your code
+    }
+
+    /**
+     * Triggered when Pico reads a single page from the list of all known pages
+     *
+     * The `$pageData` parameter consists of the following values:
+     * <pre>
+     * +----------------+--------+------------------------------------------+
+     * | Array key      | Type   | Description                              |
+     * +----------------+--------+------------------------------------------+
+     * | id             | string | relative path to the content file        |
+     * | url            | string | URL to the page                          |
+     * | title          | string | title of the page (YAML header)          |
+     * | description    | string | description of the page (YAML header)    |
+     * | author         | string | author of the page (YAML header)         |
+     * | time           | string | timestamp derived from the Date header   |
+     * | date           | string | date of the page (YAML header)           |
+     * | date_formatted | string | formatted date of the page               |
+     * | raw_content    | string | raw, not yet parsed contents of the page |
+     * | meta           | string | parsed meta data of the page             |
+     * +----------------+--------+------------------------------------------+
+     * </pre>
+     *
+     * @see    DummyPlugin::onPagesLoaded()
+     * @param  array &$pageData data of the loaded page
+     * @return void
+     */
+    public function onSinglePageLoaded(&$pageData)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has read all known pages
+     *
+     * See {@link DummyPlugin::onSinglePageLoaded()} for details about the
+     * structure of the page data.
+     *
+     * @see    Pico::getPages()
+     * @see    Pico::getCurrentPage()
+     * @see    Pico::getPreviousPage()
+     * @see    Pico::getNextPage()
+     * @param  array &$pages        data of all known pages
+     * @param  array &$currentPage  data of the page being served
+     * @param  array &$previousPage data of the previous page
+     * @param  array &$nextPage     data of the next page
+     * @return void
+     */
+    public function onPagesLoaded(&$pages, &$currentPage, &$previousPage, &$nextPage)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico registers the twig template engine
+     *
+     * @return void
+     */
+    public function onTwigRegistration()
+    {
+        // your code
+    }
+
+    /**
+     * Triggered before Pico renders the page
+     *
+     * @see    Pico::getTwig()
+     * @see    DummyPlugin::onPageRendered()
+     * @param  Twig_Environment &$twig          twig template engine
+     * @param  mixed[]          &$twigVariables template variables
+     * @param  string           &$templateName  file name of the template
+     * @return void
+     */
+    public function onPageRendering(&$twig, &$twigVariables, &$templateName)
+    {
+        // your code
+    }
+
+    /**
+     * Triggered after Pico has rendered the page
+     *
+     * @param  string &$output contents which will be sent to the user
+     * @return void
+     */
+    public function onPageRendered(&$output)
+    {
+        // your code
+    }
+}

+ 0 - 0
plugins/index.html


+ 0 - 95
plugins/pico_plugin.php

@@ -1,95 +0,0 @@
-<?php
-
-/**
- * Example hooks for a Pico plugin
- *
- * @author Gilbert Pellegrom
- * @link http://picocms.org
- * @license http://opensource.org/licenses/MIT
- */
-class Pico_Plugin
-{
-
-    public function plugins_loaded()
-    {
-
-    }
-
-    public function config_loaded(&$settings)
-    {
-
-    }
-
-    public function request_url(&$url)
-    {
-
-    }
-
-    public function before_load_content(&$file)
-    {
-
-    }
-
-    public function after_load_content(&$file, &$content)
-    {
-
-    }
-
-    public function before_404_load_content(&$file)
-    {
-
-    }
-
-    public function after_404_load_content(&$file, &$content)
-    {
-
-    }
-
-    public function before_read_file_meta(&$headers)
-    {
-
-    }
-
-    public function file_meta(&$meta)
-    {
-
-    }
-
-    public function before_parse_content(&$content)
-    {
-
-    }
-
-    public function after_parse_content(&$content)
-    {
-
-    }
-
-    public function get_page_data(&$data, $page_meta)
-    {
-
-    }
-
-    public function get_pages(&$pages, &$current_page, &$prev_page, &$next_page)
-    {
-
-    }
-
-    public function before_twig_register()
-    {
-
-    }
-
-    public function before_render(&$twig_vars, &$twig, &$template)
-    {
-
-    }
-
-    public function after_render(&$output)
-    {
-
-    }
-
-}
-
-?>

+ 0 - 48
themes/default/index.html

@@ -1,48 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" class="no-js">
-<head>
-	<meta charset="utf-8" />
-	
-	<title>{% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }}</title>
-{% if meta.description %}
-	<meta name="description" content="{{ meta.description }}"> 
-{% endif %}{% if meta.robots %}
-	<meta name="robots" content="{{ meta.robots }}">
-{% endif %}
-	
-	<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Open+Sans:400,700" type="text/css" />
-	<link rel="stylesheet" href="{{ theme_url }}/style.css" type="text/css" />
-	
-	<script src="{{ theme_url }}/scripts/modernizr-2.6.1.min.js"></script>
-</head>
-<body>
-
-	<header id="header">
-		<div class="inner clearfix">
-			<h1><a href="{{ base_url }}" id="logo">{{ site_title }}</a></h1>
-			<nav>
-				<a href="#" class="menu-icon"></a>
-				<ul>
-					{% for page in pages %}
-						<li><a href="{{ page.url }}">{{ page.title }}</a></li>
-					{% endfor %}
-				</ul>
-			</nav>
-		</div>
-	</header>
-
-	<section id="content">
-		<div class="inner">
-			{{ content }}
-		</div>
-	</section>
-	
-	<footer id="footer">
-		<div class="inner">
-			<a href="http://picocms.org/">Pico</a> was made by <a href="http://gilbert.pellegrom.me">Gilbert Pellegrom</a> 
-			from <a href="http://dev7studios.com">Dev7studios</a>.
-		</div>
-	</footer>
-    
-</body>
-</html>

+ 49 - 0
themes/default/index.twig

@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html lang="en" class="no-js">
+<head>
+    <meta charset="utf-8" />
+
+    <title>{% if meta.title %}{{ meta.title }} | {% endif %}{{ site_title }}</title>
+    {% if meta.description %}
+        <meta name="description" content="{{ meta.description|striptags }}">
+    {% endif %}{% if meta.robots %}
+        <meta name="robots" content="{{ meta.robots }}">
+    {% endif %}
+
+    <link rel="stylesheet" href="http://fonts.googleapis.com/css?family=Open+Sans:400,700" type="text/css" />
+    <link rel="stylesheet" href="{{ theme_url }}/style.css" type="text/css" />
+
+    <script src="{{ theme_url }}/scripts/modernizr-2.6.1.min.js"></script>
+</head>
+<body>
+
+    <header id="header">
+        <div class="inner clearfix">
+            <h1><a href="{{ "index"|link }}" id="logo">{{ site_title }}</a></h1>
+            <nav>
+                <a href="#" class="menu-icon"></a>
+                <ul>
+                    {% for page in pages %}
+                        <li><a href="{{ page.url }}">{{ page.title }}</a></li>
+                    {% endfor %}
+                </ul>
+            </nav>
+        </div>
+    </header>
+
+    <section id="content">
+        <div class="inner">
+            {{ content }}
+        </div>
+    </section>
+
+    <footer id="footer">
+        <div class="inner">
+            <a href="http://picocms.org/">Pico</a>
+            was made by <a href="http://gilbert.pellegrom.me">Gilbert Pellegrom</a>
+            from <a href="http://dev7studios.com">Dev7studios</a>.
+        </div>
+    </footer>
+
+</body>
+</html>

+ 0 - 0
themes/index.html