{{ page.title }}
+{{ page.date_formatted }}
+{{ page.description }}
+Physical Location | URL |
---|---|
content-sample/index.md | / |
content-sample/sub.md | /sub |
content-sample/sub/index.md | /sub (same as above) |
content-sample/sub/page.md | /sub/page |
content-sample/a/very/long/url.md | /a/very/long/url |
Physical Location | +URL | +
---|---|
content-sample/index.md | +/ | +
content-sample/sub.md | +|
content-sample/sub/index.md | +?sub (same as above) | +
content-sample/sub/page.md | +?sub/page | +
content-sample/a/very/long/url.md | +?a/very/long/url (doesn't exist) | +
%base_url%
- The URL to your Pico site
+* %site_title%
- The title of your Pico site
+* %base_url%
- The URL to your Pico site; internal links
+ can be specified using %base_url%?sub/page
+* %theme_url%
- The URL to the currently used theme
+* %meta.*%
- Access any meta variable of the current page,
+ e.g. %meta.author%
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/" %}
+
+ {% 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
-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_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.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
-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>+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 -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 diff --git a/content-sample/sub/index.md b/content-sample/sub/index.md index 517e191cf..42edf3f01 100644 --- a/content-sample/sub/index.md +++ b/content-sample/sub/index.md @@ -1,10 +1,10 @@ -/* +--- Title: 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. diff --git a/content-sample/sub/page.md b/content-sample/sub/page.md index 2e2aebd51..95d67bdfc 100644 --- a/content-sample/sub/page.md +++ b/content-sample/sub/page.md @@ -1,10 +1,10 @@ -/* +--- Title: 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. diff --git a/index.php b/index.php index b69d4acdd..40b811a0d 100644 --- a/index.php +++ b/index.php @@ -1,15 +1,17 @@ setConfig(array()); -require_once(VENDOR_DIR . 'autoload.php'); -require_once(LIB_DIR . 'pico.php'); -$pico = new Pico(); +// run application +echo $pico->run(); diff --git a/lib/AbstractPicoPlugin.php b/lib/AbstractPicoPlugin.php new file mode 100644 index 000000000..55660d2d3 --- /dev/null +++ b/lib/AbstractPicoPlugin.php @@ -0,0 +1,255 @@ +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; + } +} diff --git a/lib/Pico.php b/lib/Pico.php new file mode 100644 index 000000000..91a6c27b0 --- /dev/null +++ b/lib/Pico.php @@ -0,0 +1,1276 @@ + for more info. + * + * @author Gilbert Pellegrom + * @author Daniel Rudolf + * @link
+ * +----------------+--------+------------------------------------------+ + * | 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 | + * +----------------+--------+------------------------------------------+ + *+ * + * @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); + } + } + } + } +} diff --git a/lib/PicoPluginInterface.php b/lib/PicoPluginInterface.php new file mode 100644 index 000000000..8ea3ab67c --- /dev/null +++ b/lib/PicoPluginInterface.php @@ -0,0 +1,102 @@ +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 .= '…'; - } - - return $excerpt; - } - -} diff --git a/plugins/00-PicoDeprecated.php b/plugins/00-PicoDeprecated.php new file mode 100644 index 000000000..53bfff2d0 --- /dev/null +++ b/plugins/00-PicoDeprecated.php @@ -0,0 +1,430 @@ + + * +---------------------+-----------------------------------------------------------+ + * | 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) | + * +---------------------+-----------------------------------------------------------+ + * + * + * 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); + } + } + } +} diff --git a/plugins/01-PicoParsePagesContent.php b/plugins/01-PicoParsePagesContent.php new file mode 100644 index 000000000..fd36f0adb --- /dev/null +++ b/plugins/01-PicoParsePagesContent.php @@ -0,0 +1,40 @@ +prepareFileContent($pageData['raw_content'], $pageData['meta']); + $pageData['content'] = $this->parseFileContent($pageData['content']); + } + } +} diff --git a/plugins/02-PicoExcerpt.php b/plugins/02-PicoExcerpt.php new file mode 100644 index 000000000..4ada382d3 --- /dev/null +++ b/plugins/02-PicoExcerpt.php @@ -0,0 +1,81 @@ +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))) . '…'; + } + return $string; + } +} diff --git a/plugins/DummyPlugin.php b/plugins/DummyPlugin.php new file mode 100644 index 000000000..9ecd411d1 --- /dev/null +++ b/plugins/DummyPlugin.php @@ -0,0 +1,313 @@ + + * +----------------+--------+------------------------------------------+ + * | 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 | + * +----------------+--------+------------------------------------------+ + * + * + * @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 + } +} diff --git a/plugins/index.html b/plugins/index.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/pico_plugin.php b/plugins/pico_plugin.php deleted file mode 100644 index 236b12edd..000000000 --- a/plugins/pico_plugin.php +++ /dev/null @@ -1,95 +0,0 @@ - diff --git a/themes/default/index.html b/themes/default/index.html deleted file mode 100644 index 9b5ed0af1..000000000 --- a/themes/default/index.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - -