diff --git a/lib/Pico.php b/lib/Pico.php index be75671..b315d72 100644 --- a/lib/Pico.php +++ b/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(); } diff --git a/lib/PicoTwigExtension.php b/lib/PicoTwigExtension.php new file mode 100644 index 0000000..d62e509 --- /dev/null +++ b/lib/PicoTwigExtension.php @@ -0,0 +1,222 @@ +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; + } +}