소스 검색

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

theshka 9 년 전
부모
커밋
3b223e8e1d
6개의 변경된 파일213개의 추가작업 그리고 64개의 파일을 삭제
  1. 3 3
      README.md
  2. 63 2
      changelog.txt
  3. 9 4
      lib/AbstractPicoPlugin.php
  4. 73 21
      lib/Pico.php
  5. 64 33
      plugins/00-PicoDeprecated.php
  6. 1 1
      plugins/DummyPlugin.php

+ 3 - 3
README.md

@@ -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.
 

+ 63 - 2
changelog.txt

@@ -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

+ 9 - 4
lib/AbstractPicoPlugin.php

@@ -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;
     }
 
     /**

+ 73 - 21
lib/Pico.php

@@ -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;
                 }

+ 64 - 33
plugins/00-PicoDeprecated.php

@@ -84,14 +84,65 @@ 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)
+    {
+        $this->defineConstants();
+        $this->loadRootDirConfig($config);
+        $this->enablePlugins();
+
+        $this->triggerEvent('config_loaded', array(&$config));
+    }
+
+    /**
+     * 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());
+        }
+        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 {@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
@@ -100,10 +151,19 @@ class PicoDeprecated extends AbstractPicoPlugin
                 $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
@@ -117,35 +177,6 @@ class PicoDeprecated extends AbstractPicoPlugin
                 $plugins['PicoExcerpt']->setEnabled(true, true, true);
             }
         }
-
-        // 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
-        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', $config['content_dir']);
-        }
-        if (!defined('CONTENT_EXT')) {
-            define('CONTENT_EXT', $config['content_ext']);
-        }
-
-        $this->triggerEvent('config_loaded', array(&$config));
     }
 
     /**

+ 1 - 1
plugins/DummyPlugin.php

@@ -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