Browse Source

Add PicoTwigExtension with map and sort_by filters

Daniel Rudolf 9 years ago
parent
commit
f19adc64b9
2 changed files with 223 additions and 0 deletions
  1. 1 0
      lib/Pico.php
  2. 222 0
      lib/PicoTwigExtension.php

+ 1 - 0
lib/Pico.php

@@ -1117,6 +1117,7 @@ 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();
     }

+ 222 - 0
lib/PicoTwigExtension.php

@@ -0,0 +1,222 @@
+<?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(
+            'map' => new Twig_SimpleFilter('map', array($this, 'mapFilter')),
+            'sort_by' => new Twig_SimpleFilter('sort_by', array($this, 'sortByFilter')),
+        );
+    }
+
+    /**
+     * 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 InvalidArgumentException(
+                'Unable to apply Twig "map" filter: '
+                . 'You must pass a traversable variable'
+            );
+        }
+        if (empty($mapKeyPath)) {
+            throw new InvalidArgumentException(
+                'Unable to apply Twig "map" filter: '
+                . 'You must specify the $mapKeyPath parameter'
+            );
+        }
+
+        $result = array();
+        foreach ($var as $key => $value) {
+            $result[$key] = $this->getKeyOfVar($value, $mapKeyPath);
+        }
+        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 InvalidArgumentException(
+                'Unable to apply Twig "sort_by" filter: '
+                . 'You must pass a traversable variable'
+            );
+        }
+        if (empty($sortKeyPath)) {
+            throw new InvalidArgumentException(
+                'Unable to apply Twig "sort_by" filter: '
+                . 'You must specify the $sortKeyPath parameter'
+            );
+        }
+        if (($fallback !== 'top') && ($fallback !== 'bottom') && ($fallback !== 'keep')) {
+            throw new InvalidArgumentException(
+                'Unable to apply Twig "sort_by" filter: '
+                . 'Invalid $fallback parameter: ' . $fallback
+            );
+        }
+
+        $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 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, 'Traversable')) {
+                    $var = iterator_to_array($var);
+                } elseif (isset($var->{$key})) {
+                    $var = $var->{$key};
+                    continue;
+                } elseif (is_callable(array($var, 'get' . ucfirst($key)))) {
+                    $var = call_user_func(array($var, 'get' . ucfirst($key)));
+                    continue;
+                } elseif (!is_a($var, 'ArrayAccess')) {
+                    return null;
+                }
+            } elseif (!is_array($var)) {
+                return null;
+            }
+
+            if (isset($var[$key])) {
+                $var = $var[$key];
+                continue;
+            }
+
+            return null;
+        }
+
+        return $var;
+    }
+}