Merge branch 'pico1.0' of https://github.com/PhrozenByte/Pico into PhrozenByte-pico1.0

This commit is contained in:
theshka 2015-10-04 19:26:38 -06:00
commit 3b223e8e1d
6 changed files with 214 additions and 65 deletions

View file

@ -2,7 +2,7 @@ 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)]()
[![Version](https://img.shields.io/badge/version-1.0-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)
Pico is a stupidly simple, blazing fast, flat file CMS. See http://picocms.org/ for more info.
@ -45,7 +45,7 @@ You have nothing to consider specially, simply navigate to your Pico install usi
#### You don't have a web server?
The easiest way to 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!
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!
###### Step 1
Navigate to Picos installation directory using a shell.
@ -62,7 +62,7 @@ Access Pico from <http://localhost:8080>.
Getting Help
------------
You can read the wiki if you are looking for examples and read the inline-docs for more development information.
You can read the [wiki][Wiki] if you are looking for examples and read the inline-docs for more development information.
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.

View file

@ -1,5 +1,66 @@
*** Pico Changelog ***
2015.10.XX - version 1.0-beta
* NOTE: This changelog only provides basic information about the enormous
changes introduced with Pico 1.0-beta. Please refer to the UPGRADE
section of the docs for details.
* [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 older
* [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] 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
* [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 Picos root dir
* [Changed] Adding `Pico::run()` method that performs Picos 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] 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 Picos excerpt feature to `PicoExcerpt` plugin
2015.04.28 - version 0.9
* [New] Default theme is now mobile-friendly
* [New] Description meta now available in content areas
@ -8,13 +69,13 @@
* [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] Pico now only removes the 1st comment block in .md files
* [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] 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

View file

@ -71,9 +71,14 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
{
// plugins can be enabled/disabled using the config
if ($eventName === 'onConfigLoaded') {
$pluginEnabled = $this->getConfig(get_called_class().'.enabled');
$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']);
}
}
}
@ -137,8 +142,8 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
}
throw new BadMethodCallException(
'Call to undefined method '.get_class($this->getPico()).'::'.$methodName.'() '
. 'through '.get_called_class().'::__call()'
'Call to undefined method ' . get_class($this->getPico()) . '::' . $methodName . '() '
. 'through ' . get_called_class() . '::__call()'
);
}
@ -187,7 +192,7 @@ abstract class AbstractPicoPlugin implements PicoPluginInterface
*/
public function getDependencies()
{
return $this->dependsOn;
return (array) $this->dependsOn;
}
/**

View file

@ -26,6 +26,30 @@
*/
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
*
@ -247,7 +271,7 @@ class Pico
$this->triggerEvent('on404ContentLoading', array(&$this->requestFile));
header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
$this->rawContent = $this->load404Content();
$this->rawContent = $this->load404Content($this->requestFile);
$this->triggerEvent('on404ContentLoaded', array(&$this->rawContent));
}
@ -274,6 +298,7 @@ class Pico
$this->triggerEvent('onPagesLoading');
$this->readPages();
$this->sortPages();
$this->discoverCurrentPage();
$this->triggerEvent('onPagesLoaded', array(
@ -356,7 +381,7 @@ class Pico
return $this->plugins[$pluginName];
}
throw new RuntimeException("Missing plugin '".$pluginName."'");
throw new RuntimeException("Missing plugin '" . $pluginName . "'");
}
/**
@ -529,13 +554,27 @@ class Pico
}
/**
* Returns the raw contents of the 404 file if the requested file wasn't found
* Returns the raw contents of the first found 404 file when traversing
* up from the directory the requested file is in
*
* @return string raw contents of the 404 file
* @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()
public function load404Content($file)
{
return $this->loadFileContent($this->getConfig('content_dir') . '404' . $this->getConfig('content_ext'));
$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);
}
/**
@ -577,9 +616,10 @@ class Pico
*
* 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. You MUST register
* new headers during the `onMetaHeaders` event first, otherwise they are
* ignored and won't be returned.
* 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 <http://symfony.com/doc/current/components/yaml/introduction.html>
* @param string $rawContent the raw file contents
@ -593,16 +633,19 @@ class Pico
. "(.*?)(?:\r)?\n(?(2)\*\/|---)[[:blank:]]*(?:(?:\r)?\n|$)/s";
if (preg_match($pattern, $rawContent, $rawMetaMatches)) {
$yamlParser = new \Symfony\Component\Yaml\Parser();
$rawMeta = $yamlParser->parse($rawMetaMatches[3]);
$rawMeta = array_change_key_case($rawMeta, CASE_LOWER);
$meta = $yamlParser->parse($rawMetaMatches[3]);
$meta = array_change_key_case($meta, CASE_LOWER);
// TODO: maybe we should change this to pass all headers, no matter
// they are registered during the `onMetaHeaders` event or not...
foreach ($headers as $fieldId => $fieldName) {
$fieldName = strtolower($fieldName);
if (isset($rawMeta[$fieldName])) {
$meta[$fieldId] = $rawMeta[$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] = '';
}
}
@ -614,6 +657,7 @@ class Pico
$meta['time'] = $meta['date_formatted'] = '';
}
} else {
// guarantee array key existance
foreach ($headers as $id => $field) {
$meta[$id] = '';
}
@ -708,7 +752,7 @@ class Pico
protected function readPages()
{
$this->pages = array();
$files = $this->getFiles($this->getConfig('content_dir'), $this->getConfig('content_ext'), SCANDIR_SORT_NONE);
$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')) {
@ -760,7 +804,15 @@ class Pico
$this->pages[$id] = $page;
}
}
/**
* Sorts all pages known to Pico
*
* @return void
*/
protected function sortPages()
{
// sort pages
$order = $this->getConfig('pages_order');
$alphaSortClosure = function ($a, $b) use ($order) {
@ -990,12 +1042,12 @@ class Pico
* @param string $fileExtension return files with the given file extension
* only (optional)
* @param int $order specify whether and how files should be
* sorted; use SCANDIR_SORT_ASCENDING for a alphabetical ascending
* order (default), SCANDIR_SORT_DESCENDING for a descending order or
* SCANDIR_SORT_NONE to leave the result unsorted
* 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 = SCANDIR_SORT_ASCENDING)
protected function getFiles($directory, $fileExtension = '', $order = self::SORT_ASC)
{
$directory = rtrim($directory, '/');
$result = array();
@ -1013,7 +1065,7 @@ class Pico
if (is_dir($directory . '/' . $file)) {
// get files recursively
$result = array_merge($result, $this->getFiles($directory . '/' . $file, $fileExtension));
$result = array_merge($result, $this->getFiles($directory . '/' . $file, $fileExtension, $order));
} elseif (empty($fileExtension) || (substr($file, -$fileExtensionLength) === $fileExtension)) {
$result[] = $directory . '/' . $file;
}

View file

@ -84,44 +84,34 @@ class PicoDeprecated extends AbstractPicoPlugin
}
/**
* Triggers the deprecated event config_loaded($config), tries to read
* {@path "config.php"} in Picos root dir, enables the plugins
* {@link PicoParsePagesContent} and {@link PicoExcerpt} and defines some
* deprecated constants (ROOT_DIR, CONTENT_DIR etc.)
* Triggers the deprecated event config_loaded($config)
*
* @see PicoDeprecated::defineConstants()
* @see PicoDeprecated::loadRootDirConfig()
* @see PicoDeprecated::enablePlugins()
* @see DummyPlugin::onConfigLoaded()
*/
public function onConfigLoaded(&$config)
{
if (file_exists($this->getRootDir() . 'config.php')) {
// config.php in Pico::$rootDir is deprecated; use Pico::$configDir instead
$newConfig = require($this->getRootDir() . 'config.php');
if (is_array($newConfig)) {
$config = $newConfig + $config;
}
}
$this->defineConstants();
$this->loadRootDirConfig($config);
$this->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
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);
}
}
$this->triggerEvent('config_loaded', array(&$config));
}
// CONTENT_DIR constant is deprecated since v0.9,
// ROOT_DIR, LIB_DIR, PLUGINS_DIR, THEMES_DIR and CONTENT_EXT constants since v1.0,
// CONFIG_DIR constant existed just for a short time between v0.9 and v1.0,
// CACHE_DIR constant was dropped with v1.0 without a replacement
/**
* Defines deprecated constants
*
* CONTENT_DIR is deprecated since v0.9, ROOT_DIR, LIB_DIR, PLUGINS_DIR,
* THEMES_DIR and CONTENT_EXT since v1.0, CONFIG_DIR existed just for a
* short time between v0.9 and v1.0 and CACHE_DIR was dropped with v1.0
* without a replacement.
*
* @return void
*/
protected function defineConstants()
{
if (!defined('ROOT_DIR')) {
define('ROOT_DIR', $this->getRootDir());
}
@ -139,13 +129,54 @@ class PicoDeprecated extends AbstractPicoPlugin
define('THEMES_DIR', $this->getThemesDir());
}
if (!defined('CONTENT_DIR')) {
define('CONTENT_DIR', $config['content_dir']);
define('CONTENT_DIR', $this->getConfig('content_dir'));
}
if (!defined('CONTENT_EXT')) {
define('CONTENT_EXT', $config['content_ext']);
define('CONTENT_EXT', $this->getConfig('content_ext'));
}
}
$this->triggerEvent('config_loaded', array(&$config));
/**
* Read {@path "config.php"} in Picos root dir
*
* @param array &$config array of config variables
* @return void
*/
protected function loadRootDirConfig(&$config)
{
if (file_exists($this->getRootDir() . 'config.php')) {
// config.php in Pico::$rootDir is deprecated; use Pico::$configDir instead
$newConfig = require($this->getRootDir() . 'config.php');
if (is_array($newConfig)) {
$config = $newConfig + $config;
}
}
}
/**
* Enables the plugins {@link PicoParsePagesContent} and {@link 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);
}
}
}
/**

View file

@ -44,7 +44,7 @@ class DummyPlugin extends AbstractPicoPlugin
}
/**
* Triggered after Pico readed its configuration
* Triggered after Pico read its configuration
*
* @see Pico::getConfig()
* @param array &$config array of config variables