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

This commit is contained in:
theshka 2015-11-29 20:13:47 -06:00
commit c4f9eb8d4e
14 changed files with 350 additions and 78 deletions

10
.gitignore vendored
View file

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

View file

@ -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
.phpdoc.xml Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

24
_build/generate-phpdoc.sh Executable file
View file

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

View file

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

View file

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

View file

@ -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
lib/PicoTwigExtension.php Normal file
View file

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