Explorar el Código

Merge branch 'master' of https://github.com/picocms/Pico into feature/bootstrap-theme

theshka hace 9 años
padre
commit
c4f9eb8d4e

+ 6 - 4
.gitignore

@@ -10,15 +10,17 @@ desktop.ini
 .DS_Store
 ._*
 
-# Travis
-/build/phpdoc-*/
-/build/phpdoc-*.git/
-
 # Composer
 /composer.lock
 /composer.phar
 /vendor
 
+# phpDocumentor
+/_build/phpdoc/
+/_build/phpdoc.cache/
+/_build/phpdoc-*/
+/_build/phpdoc-*.git/
+
 # User config
 /config/config.php
 

+ 7 - 2
phpcs.xml → .phpcs.xml

@@ -6,9 +6,14 @@
     </description>
 
     <!--
-        Exclude build/ and vendor/ dirs as well as minified JavaScript files
+        Run on current working directory by default
     -->
-    <exclude-pattern type="relative">^build/</exclude-pattern>
+    <file>.</file>
+
+    <!--
+        Exclude _build/ and vendor/ dirs as well as minified JavaScript files
+    -->
+    <exclude-pattern type="relative">^_build/</exclude-pattern>
     <exclude-pattern type="relative">^vendor/</exclude-pattern>
     <exclude-pattern>*.min.js</exclude-pattern>
 

+ 32 - 0
.phpdoc.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<phpdoc>
+    <title><![CDATA[Pico 1.0 API Documentation]]></title>
+    <parser>
+        <target>_build/phpdoc.cache</target>
+    </parser>
+    <transformer>
+        <target>_build/phpdoc</target>
+    </transformer>
+    <transformations>
+        <template name="clean"/>
+    </transformations>
+    <files>
+        <directory>.</directory>
+        <file>index.php</file>
+        <file>index.php.dist</file>
+
+        <!-- exclude build environment -->
+        <ignore>_build/*</ignore>
+
+        <!-- exclude user config -->
+        <ignore>config/*</ignore>
+        <file>config/config.php.template</file>
+
+        <!-- exclude all plugins -->
+        <ignore>plugins/*</ignore>
+        <file>plugins/DummyPlugin.php</file>
+
+        <!-- exclude vendor dir -->
+        <ignore>vendor/*</ignore>
+    </files>
+</phpdoc>

+ 2 - 2
.travis.yml

@@ -17,10 +17,10 @@ install:
   - composer install
 
 before_script:
-  - export PATH="$TRAVIS_BUILD_DIR/build:$TRAVIS_BUILD_DIR/vendor/bin:$PATH"
+  - export PATH="$TRAVIS_BUILD_DIR/_build:$TRAVIS_BUILD_DIR/vendor/bin:$PATH"
 
 script:
-  - phpcs --standard=phpcs.xml "$TRAVIS_BUILD_DIR"
+  - phpcs --standard=.phpcs.xml "$TRAVIS_BUILD_DIR"
 
 after_success:
   - deploy-phpdoc-branch.sh

+ 3 - 0
CHANGELOG.md

@@ -7,8 +7,11 @@ Released: -
 ```
 * [New] This is Picos first stable release! The Pico Community wants to thank
         all contributors and users which made this possible!
+* [New] Introducing the `PicoTwigExtension` Twig extension
 * [New] New `markdown` filter for Twig to parse markdown strings; Note: If you
         want to parse the contents of a page, use the `content` filter instead
+* [New] New `sort_by` filter to sort a array by a specified key or key path
+* [New] New `map` filter to get the values of the given key or key path
 * [New] New PHP version check in `index.php`
 * [Changed] Improve documentation
 * [Changed] Improve table styling in default theme

+ 3 - 3
CONTRIBUTING.md

@@ -39,11 +39,11 @@ Pico uses the [PSR-2 Coding Standard](http://www.php-fig.org/psr/psr-2/) as defi
 
 For historical reasons we don't use formal namespaces. Markdown files in the `content-sample` folder (the inline documentation) must follow a hard limit of 80 characters line length.
 
-It is recommended to check your code using [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) using the `PSR2` standard using the following command:
+It is recommended to check your code using [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) using Pico's `.phpcs.xml` standard. Use the following command:
 
-    $ ./bin/phpcs --standard=PSR2 [file(s)]
+    $ ./vendor/bin/phpcs --standard=.phpcs.xml [file]...
 
-With this command you can specify a file or folder to limit which files it will check or omit that argument altogether, in which case the current directory is checked.
+With this command you can specify a file or folder to limit which files it will check or omit that argument altogether, in which case the current working directory is checked.
 
 ### Keep documentation in sync
 

+ 4 - 2
build/deploy-phpdoc-branch.sh → _build/deploy-phpdoc-branch.sh

@@ -18,11 +18,13 @@ fi
 PHPDOC_ID="${TRAVIS_BRANCH//\//_}"
 
 generate-phpdoc.sh \
-    "$TRAVIS_BUILD_DIR" "$TRAVIS_BUILD_DIR/build/phpdoc-$PHPDOC_ID" \
+    "$TRAVIS_BUILD_DIR/.phpdoc.xml" \
+    "$TRAVIS_BUILD_DIR/_build/phpdoc.cache" \
+    "$TRAVIS_BUILD_DIR/_build/phpdoc-$PHPDOC_ID" \
     "Pico 1.0 API Documentation ($TRAVIS_BRANCH branch)"
 [ $? -eq 0 ] || exit 1
 
 deploy-phpdoc.sh \
-    "$TRAVIS_REPO_SLUG" "heads/$TRAVIS_BRANCH @ $TRAVIS_COMMIT" "$TRAVIS_BUILD_DIR/build/phpdoc-$PHPDOC_ID" \
+    "$TRAVIS_REPO_SLUG" "heads/$TRAVIS_BRANCH @ $TRAVIS_COMMIT" "$TRAVIS_BUILD_DIR/_build/phpdoc-$PHPDOC_ID" \
     "$TRAVIS_REPO_SLUG" "gh-pages" "phpDoc/$PHPDOC_ID"
 [ $? -eq 0 ] || exit 1

+ 4 - 2
build/deploy-phpdoc-release.sh → _build/deploy-phpdoc-release.sh

@@ -5,11 +5,13 @@
 PHPDOC_ID="${TRAVIS_BRANCH//\//_}"
 
 generate-phpdoc.sh \
-    "$TRAVIS_BUILD_DIR" "$TRAVIS_BUILD_DIR/build/phpdoc-$PHPDOC_ID" \
+    "$TRAVIS_BUILD_DIR/.phpdoc.xml" \
+    "$TRAVIS_BUILD_DIR/_build/phpdoc.cache" \
+    "$TRAVIS_BUILD_DIR/_build/phpdoc-$PHPDOC_ID" \
     "Pico 1.0 API Documentation ($TRAVIS_TAG)"
 [ $? -eq 0 ] || exit 1
 
 deploy-phpdoc.sh \
-    "$TRAVIS_REPO_SLUG" "tags/$TRAVIS_TAG" "$TRAVIS_BUILD_DIR/build/phpdoc-$PHPDOC_ID" \
+    "$TRAVIS_REPO_SLUG" "tags/$TRAVIS_TAG" "$TRAVIS_BUILD_DIR/_build/phpdoc-$PHPDOC_ID" \
     "$TRAVIS_REPO_SLUG" "gh-pages" "phpDoc/$PHPDOC_ID"
 [ $? -eq 0 ] || exit 1

+ 0 - 0
build/deploy-phpdoc.sh → _build/deploy-phpdoc.sh


+ 24 - 0
_build/generate-phpdoc.sh

@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+set -e
+
+# parameters
+PHPDOC_CONFIG="$1"
+PHPDOC_CACHE_DIR="$2"
+PHPDOC_TARGET_DIR="$3"
+PHPDOC_TITLE="$4"
+
+# print parameters
+echo "Generating phpDocs..."
+printf 'PHPDOC_CONFIG="%s"\n' "$PHPDOC_CONFIG"
+printf 'PHPDOC_CACHE_DIR="%s"\n' "$PHPDOC_CACHE_DIR"
+printf 'PHPDOC_TARGET_DIR="%s"\n' "$PHPDOC_TARGET_DIR"
+printf 'PHPDOC_TITLE="%s"\n' "$PHPDOC_TITLE"
+echo
+
+# generate phpdoc
+phpdoc --config "$PHPDOC_CONFIG" \
+    --cache-folder "$PHPDOC_CACHE_DIR" \
+    --target "$PHPDOC_TARGET_DIR" \
+    --title "$PHPDOC_TITLE"
+
+echo

+ 0 - 24
build/generate-phpdoc.sh

@@ -1,24 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-# parameters
-PHPDOC_SOURCE_DIR="$1"
-PHPDOC_TARGET_DIR="$2"
-PHPDOC_TITLE="$3"
-
-# print parameters
-echo "Generating phpDocs..."
-printf 'PHPDOC_SOURCE_DIR="%s"\n' "$PHPDOC_SOURCE_DIR"
-printf 'PHPDOC_TARGET_DIR="%s"\n' "$PHPDOC_TARGET_DIR"
-printf 'PHPDOC_TITLE="%s"\n' "$PHPDOC_TITLE"
-echo
-
-# generate phpdoc
-phpdoc -d "$PHPDOC_SOURCE_DIR" \
-    -i "$PHPDOC_SOURCE_DIR/build/" \
-    -i "$PHPDOC_SOURCE_DIR/vendor/" \
-    -i "$PHPDOC_SOURCE_DIR/plugins/" -f "$PHPDOC_SOURCE_DIR/plugins/DummyPlugin.php" \
-    -t "$PHPDOC_TARGET_DIR" \
-    --title "$PHPDOC_TITLE"
-
-echo

+ 16 - 11
content-sample/index.md

@@ -117,7 +117,7 @@ something like the following:
    This template will show a list of your articles, so you probably want to
    do something like this:
    ```
-    {% for page in pages %}
+    {% for page in pages|sort_by("time")|reverse %}
         {% if page.id starts with "blog/" %}
             <div class="post">
                 <h3><a href="{{ page.url }}">{{ page.title }}</a></h3>
@@ -127,16 +127,10 @@ something like the following:
         {% 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
+4. 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.
+   to the navigation loop (`{% for page in pages|reverse %}...{% endfor %}`)
+   in your themes `index.twig`.
 
 ## Customization
 
@@ -162,7 +156,7 @@ 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`
+* `{{ config }}` - Contains 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; use Twigs `link` filter to
@@ -211,6 +205,17 @@ Pages can be used like the following:
         {% endfor %}
     </ul>
 
+Additional to Twigs extensive list of filters, functions and tags, Pico also
+provides some useful additional filters to make theming easier. You can parse
+any Markdown string to HTML using the `markdown` filter. Arrays can be sorted
+by one of its keys or a arbitrary deep sub-key using the `sort_by` filter
+(e.g. `{% for page in pages|sort_by("meta:nav"|split(":")) %}...{% endfor %}`
+iterates through all pages, ordered by the `nav` meta header; please note the
+`"meta:nav"|split(":")` part of the example, which passes `['meta', 'nav']` to
+the filter describing a key path). You can return all values of a given key or
+key path of an array using the `map` filter (e.g. `{{ pages|map("title") }}`
+returns all page titles).
+
 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

+ 11 - 28
lib/Pico.php

@@ -774,7 +774,7 @@ class Pico
                         $meta[$fieldId] = $meta[$fieldName];
                         unset($meta[$fieldName]);
                     }
-                } else {
+                } elseif (!isset($meta[$fieldId])) {
                     // guarantee array key existance
                     $meta[$fieldId] = '';
                 }
@@ -788,10 +788,7 @@ class Pico
             }
         } else {
             // guarantee array key existance
-            foreach ($headers as $id => $field) {
-                $meta[$id] = '';
-            }
-
+            $meta = array_fill_keys(array_keys($headers), '');
             $meta['time'] = $meta['date_formatted'] = '';
         }
 
@@ -1121,6 +1118,9 @@ class Pico
     /**
      * Registers the twig template engine
      *
+     * This method also registers Picos core Twig filters `link` and `content`
+     * as well as Picos {@link PicoTwigExtension} Twig extension.
+     *
      * @see    Pico::getTwig()
      * @return void
      */
@@ -1129,23 +1129,15 @@ class Pico
         $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());
+        $this->twig->addExtension(new PicoTwigExtension($this));
 
-        $this->registerTwigFilter();
-    }
-
-    /**
-     * Registers Picos additional Twig filters
-     *
-     * @return void
-     */
-    protected function registerTwigFilter()
-    {
-        $pico = $this;
-
-        // link filter
+        // register link filter
         $this->twig->addFilter(new Twig_SimpleFilter('link', array($this, 'getPageUrl')));
 
-        // content filter
+        // register content filter
+        // we pass the $pages array by reference to prevent multiple parser runs for the same page
+        // this is the reason why we can't register this filter as part of PicoTwigExtension
+        $pico = $this;
         $pages = &$this->pages;
         $this->twig->addFilter(new Twig_SimpleFilter('content', function ($page) use ($pico, &$pages) {
             if (isset($pages[$page])) {
@@ -1158,15 +1150,6 @@ class Pico
             }
             return null;
         }));
-
-        // markdown filter
-        $this->twig->addFilter(new Twig_SimpleFilter('markdown', function ($markdown) use ($pico) {
-            if ($pico->getParsedown() === null) {
-                throw new LogicException("Unable to parse file contents: Parsedown instance wasn't registered yet");
-            }
-
-            return $pico->getParsedown()->text($markdown);
-        }));
     }
 
     /**

+ 238 - 0
lib/PicoTwigExtension.php

@@ -0,0 +1,238 @@
+<?php
+
+/**
+ * Picos Twig extension to implement additional filters
+ *
+ * @author  Daniel Rudolf
+ * @link    http://picocms.org
+ * @license http://opensource.org/licenses/MIT
+ * @version 1.0
+ */
+class PicoTwigExtension extends Twig_Extension
+{
+    /**
+     * Current instance of Pico
+     *
+     * @see PicoTwigExtension::getPico()
+     * @var Pico
+     */
+    private $pico;
+
+    /**
+     * Constructs a new instance of this Twig extension
+     *
+     * @param Pico $pico current instance of Pico
+     */
+    public function __construct(Pico $pico)
+    {
+        $this->pico = $pico;
+    }
+
+    /**
+     * Returns the extensions instance of Pico
+     *
+     * @see    Pico
+     * @return Pico the extensions instance of Pico
+     */
+    public function getPico()
+    {
+        return $this->pico;
+    }
+
+    /**
+     * Returns the name of the extension
+     *
+     * @see    Twig_ExtensionInterface::getName()
+     * @return string the extension name
+     */
+    public function getName()
+    {
+        return 'PicoTwigExtension';
+    }
+
+    /**
+     * Returns the Twig filters markdown, map and sort_by
+     *
+     * @see    Twig_ExtensionInterface::getFilters()
+     * @return Twig_SimpleFilter[] array of Picos Twig filters
+     */
+    public function getFilters()
+    {
+        return array(
+            'markdown' => new Twig_SimpleFilter('markdown', array($this, 'markdownFilter')),
+            'map' => new Twig_SimpleFilter('map', array($this, 'mapFilter')),
+            'sort_by' => new Twig_SimpleFilter('sort_by', array($this, 'sortByFilter')),
+        );
+    }
+
+    /**
+     * Parses a markdown string to HTML
+     *
+     * This method is registered as the Twig `markdown` filter. You can use it
+     * to e.g. parse a meta variable (`{{ meta.description|markdown }}`).
+     * Don't use it to parse the contents of a page, use the `content` filter
+     * instead, what ensures the proper preparation of the contents.
+     *
+     * @param  string $markdown markdown to parse
+     * @return string           parsed HTML
+     */
+    public function markdownFilter($markdown)
+    {
+        if ($this->getPico()->getParsedown() === null) {
+            throw new LogicException(
+                'Unable to apply Twig "markdown" filter: '
+                . 'Parsedown instance wasn\'t registered yet'
+            );
+        }
+
+        return $this->getPico()->getParsedown()->text($markdown);
+    }
+
+    /**
+     * Returns a array with the values of the given key or key path
+     *
+     * This method is registered as the Twig `map` filter. You can use this
+     * filter to e.g. get all page titles (`{{ pages|map("title") }}`).
+     *
+     * @param  array|Traversable $var        variable to map
+     * @param  mixed             $mapKeyPath key to map; either a scalar or a
+     *     array interpreted as key path (i.e. ['foo', 'bar'] will return all
+     *     $item['foo']['bar'] values)
+     * @return array                         mapped values
+     */
+    public function mapFilter($var, $mapKeyPath)
+    {
+        if (!is_array($var) && (!is_object($var) || !is_a($var, 'Traversable'))) {
+            throw new Twig_Error_Runtime(sprintf(
+                'The map filter only works with arrays or "Traversable", got "%s"',
+                is_object($var) ? get_class($var) : gettype($var)
+            ));
+        }
+
+        $result = array();
+        foreach ($var as $key => $value) {
+            $mapValue = $this->getKeyOfVar($value, $mapKeyPath);
+            $result[$key] = ($mapValue !== null) ? $mapValue : $value;
+        }
+        return $result;
+    }
+
+    /**
+     * Sorts an array by one of its keys or a arbitrary deep sub-key
+     *
+     * This method is registered as the Twig `sort_by` filter. You can use this
+     * filter to e.g. sort the pages array by a arbitrary meta value. Calling
+     * `{{ pages|sort_by("meta:nav"|split(":")) }}` returns all pages sorted by
+     * the meta value `nav`. Please note the `"meta:nav"|split(":")` part of
+     * the example. The sorting algorithm will never assume equality of two
+     * values, it will then fall back to the original order. The result is
+     * always sorted in ascending order, apply Twigs `reverse` filter to
+     * achieve a descending order.
+     *
+     * @param  array|Traversable $var         variable to sort
+     * @param  mixed             $sortKeyPath key to use for sorting; either
+     *     a scalar or a array interpreted as key path (i.e. ['foo', 'bar']
+     *     will sort $var by $item['foo']['bar'])
+     * @param  string            $fallback    specify what to do with items
+     *     which don't contain the specified sort key; use "bottom" (default)
+     *     to move those items to the end of the sorted array, "top" to rank
+     *     them first, or "keep" to keep the original order of those items
+     * @return array                          sorted array
+     */
+    public function sortByFilter($var, $sortKeyPath, $fallback = 'bottom')
+    {
+        if (is_object($var) && is_a($var, 'Traversable')) {
+            $var = iterator_to_array($var, true);
+        } elseif (!is_array($var)) {
+            throw new Twig_Error_Runtime(sprintf(
+                'The sort_by filter only works with arrays or "Traversable", got "%s"',
+                is_object($var) ? get_class($var) : gettype($var)
+            ));
+        }
+        if (($fallback !== 'top') && ($fallback !== 'bottom') && ($fallback !== 'keep')) {
+            throw new Twig_Error_Runtime('The sort_by filter only supports the "top", "bottom" and "keep" fallbacks');
+        }
+
+        $twigExtension = $this;
+        $varKeys = array_keys($var);
+        uksort($var, function ($a, $b) use ($twigExtension, $var, $varKeys, $sortKeyPath, $fallback, &$removeItems) {
+            $aSortValue = $twigExtension->getKeyOfVar($var[$a], $sortKeyPath);
+            $aSortValueNull = ($aSortValue === null);
+
+            $bSortValue = $twigExtension->getKeyOfVar($var[$b], $sortKeyPath);
+            $bSortValueNull = ($bSortValue === null);
+
+            if ($aSortValueNull xor $bSortValueNull) {
+                if ($fallback === 'top') {
+                    return ($aSortValueNull - $bSortValueNull) * -1;
+                } elseif ($fallback === 'bottom') {
+                    return ($aSortValueNull - $bSortValueNull);
+                }
+            } elseif (!$aSortValueNull && !$bSortValueNull) {
+                if ($aSortValue != $bSortValue) {
+                    return ($aSortValue > $bSortValue) ? 1 : -1;
+                }
+            }
+
+            // never assume equality; fallback to original order
+            $aIndex = array_search($a, $varKeys);
+            $bIndex = array_search($b, $varKeys);
+            return ($aIndex > $bIndex) ? 1 : -1;
+        });
+
+        return $var;
+    }
+
+    /**
+     * Returns the value of a variable item specified by a scalar key or a
+     * arbitrary deep sub-key using a key path
+     *
+     * @param  array|Traversable|ArrayAccess|object $var     base variable
+     * @param  mixed                                $keyPath scalar key or a
+     *     array interpreted as key path (when passing e.g. ['foo', 'bar'],
+     *     the method will return $var['foo']['bar']) specifying the value
+     * @return mixed                                         the requested
+     *     value or NULL when the given key or key path didn't match
+     */
+    public static function getKeyOfVar($var, $keyPath)
+    {
+        if (empty($keyPath)) {
+            return null;
+        } elseif (!is_array($keyPath)) {
+            $keyPath = array($keyPath);
+        }
+
+        foreach ($keyPath as $key) {
+            if (is_object($var)) {
+                if (is_a($var, 'ArrayAccess')) {
+                    // use ArrayAccess, see below
+                } elseif (is_a($var, 'Traversable')) {
+                    $var = iterator_to_array($var);
+                } elseif (isset($var->{$key})) {
+                    $var = $var->{$key};
+                    continue;
+                } elseif (is_callable(array($var, 'get' . ucfirst($key)))) {
+                    try {
+                        $var = call_user_func(array($var, 'get' . ucfirst($key)));
+                        continue;
+                    } catch (BadMethodCallException $e) {
+                        return null;
+                    }
+                } else {
+                    return null;
+                }
+            } elseif (!is_array($var)) {
+                return null;
+            }
+
+            if (isset($var[$key])) {
+                $var = $var[$key];
+                continue;
+            }
+
+            return null;
+        }
+
+        return $var;
+    }
+}