🎉 Add Pico theme API versioning and add pico-theme.yml

- Add pico-theme.yml with a theme's API version, theme-specific default Twig config, registering theme-specific custom meta headers and defaults for Pico's `theme_config` config
- Add new `onThemeLoading(&$theme)` and `onThemeLoaded($theme, $themeApiVersion, &$themeConfig)` events
- Enable Twig autoescaping by default
This commit is contained in:
Daniel Rudolf 2019-09-22 18:49:37 +02:00
parent c1113a780c
commit b27b4f388a
No known key found for this signature in database
GPG key ID: A061F02CD8DE4538
3 changed files with 180 additions and 25 deletions

View file

@ -17,8 +17,9 @@ themes_url: ~ # Pico will try to guess the URL to the them
theme_config: # Additional theme-specific config
widescreen: false # Default theme: Use more horicontal space (i.e. make the site container wider)
twig_config: # Twig template engine config
autoescape: false # Let Twig escape variables by default
autoescape: html # Let Twig escape variables by default
strict_variables: false # If set to true, Twig will bail out when unset variables are being used
charset: utf-8 # The charset used by Twig templates
debug: ~ # Enable Twig's debug mode
cache: false # Enable Twig template caching by specifying a path to a writable directory
auto_reload: ~ # Recompile Twig templates whenever the source code changes

View file

@ -168,6 +168,29 @@ class Pico
*/
protected $config;
/**
* Theme in use
*
* @see Pico::getTheme()
* @var string
*/
protected $theme;
/**
* API version of the current theme
*
* @see Pico::getThemeApiVersion()
* @var int
*/
protected $themeApiVersion;
/**
* Additional meta headers of the current theme
*
* @var array<string,string>|null
*/
protected $themeMetaHeaders;
/**
* Part of the URL describing the requested contents
*
@ -411,6 +434,16 @@ class Pico
throw new RuntimeException('Invalid content directory "' . $this->getConfig('content_dir') . '"');
}
// load theme
$this->theme = $this->config['theme'];
$this->triggerEvent('onThemeLoading', array(&$this->theme));
$this->loadTheme();
$this->triggerEvent(
'onThemeLoaded',
array($this->theme, $this->themeApiVersion, &$this->config['theme_config'])
);
// evaluate request url
$this->evaluateRequestUrl();
$this->triggerEvent('onRequestUrl', array(&$this->requestUrl));
@ -903,6 +936,8 @@ class Pico
'debug' => null,
'timezone' => null,
'theme' => 'default',
'theme_config' => null,
'theme_meta' => null,
'themes_url' => null,
'twig_config' => null,
'date_format' => '%D %T',
@ -950,27 +985,6 @@ class Pico
$this->config['themes_url'] = $this->getAbsoluteUrl($this->config['themes_url']);
}
$defaultTwigConfig = array(
'autoescape' => false,
'strict_variables' => false,
'debug' => null,
'cache' => false,
'auto_reload' => null
);
if (!is_array($this->config['twig_config'])) {
$this->config['twig_config'] = $defaultTwigConfig;
} else {
$this->config['twig_config'] += $defaultTwigConfig;
if ($this->config['twig_config']['cache']) {
$this->config['twig_config']['cache'] = $this->getAbsolutePath($this->config['twig_config']['cache']);
}
if ($this->config['twig_config']['debug'] === null) {
$this->config['twig_config']['debug'] = $this->isDebugModeEnabled();
}
}
if (!$this->config['content_dir']) {
// try to guess the content directory
if (is_file($this->getRootDir() . 'content/index' . $this->config['content_ext'])) {
@ -1056,6 +1070,113 @@ class Pico
}
}
/**
* Loads a theme's config file (pico-theme.yml)
*
* @see Pico::getTheme()
* @see Pico::getThemeApiVersion()
*/
protected function loadTheme()
{
$themeConfig = array();
// load theme config from pico-theme.yml
$themeConfigFile = $this->getThemesDir() . $this->getTheme() . '/pico-theme.yml';
if (is_file($themeConfigFile)) {
$yamlParser = $this->getYamlParser();
$loadConfigClosure = function ($configFile) use ($yamlParser) {
$yaml = file_get_contents($configFile);
$config = $yamlParser->parse($yaml);
return is_array($config) ? $config : array();
};
$themeConfig = $loadConfigClosure($themeConfigFile);
}
$themeConfig += array(
'api_version' => null,
'meta' => array(),
'twig_config' => array()
);
// theme API version
if (preg_match('/^[0-9]+$/', $themeConfig['api_version'])) {
$this->themeApiVersion = (int) $themeConfig['api_version'];
} else {
$this->themeApiVersion = 0;
}
unset($themeConfig['api_version']);
// twig config
$themeTwigConfig = array('autoescape' => 'html', 'strict_variables' => false, 'charset' => 'utf-8');
foreach ($themeTwigConfig as $key => $_) {
if (isset($themeConfig['twig_config'][$key])) {
$themeTwigConfig[$key] = $themeConfig['twig_config'][$key];
}
}
unset($themeConfig['twig_config']);
$defaultTwigConfig = array('debug' => null, 'cache' => false, 'auto_reload' => null);
$this->config['twig_config'] = array_merge($defaultTwigConfig, $themeTwigConfig, $this->config['twig_config']);
if ($this->config['twig_config']['autoescape'] === true) {
$this->config['twig_config']['autoescape'] = 'html';
}
if ($this->config['twig_config']['cache']) {
$this->config['twig_config']['cache'] = $this->getAbsolutePath($this->config['twig_config']['cache']);
}
if ($this->config['twig_config']['debug'] === null) {
$this->config['twig_config']['debug'] = $this->isDebugModeEnabled();
}
// meta headers
$this->themeMetaHeaders = is_array($themeConfig['meta']) ? $themeConfig['meta'] : array();
unset($themeConfig['meta']);
// theme config
if (!is_array($this->config['theme_config'])) {
$this->config['theme_config'] = $themeConfig;
} else {
$this->config['theme_config'] += $themeConfig;
}
// check for theme compatibility
if (!isset($this->plugins['PicoDeprecated']) && ($this->themeApiVersion < static::API_VERSION)) {
throw new RuntimeException(
'Current theme "' . $this->theme . '" uses API version ' . $this->themeApiVersion . ', but Pico '
. 'provides API version ' . static::API_VERSION . ' and PicoDeprecated isn\'t loaded'
);
}
}
/**
* Returns the name of the current theme
*
* @see Pico::loadTheme()
* @see Pico::getThemeApiVersion()
*
* @return string
*/
public function getTheme()
{
return $this->theme;
}
/**
* Returns the API version of the current theme
*
* @see Pico::loadTheme()
* @see Pico::getTheme()
*
* @return int
*/
public function getThemeApiVersion()
{
return $this->themeApiVersion;
}
/**
* Evaluates the requested URL
*
@ -1305,6 +1426,10 @@ class Pico
'Hidden' => 'hidden'
);
if ($this->themeMetaHeaders) {
$this->metaHeaders += $this->themeMetaHeaders;
}
$this->triggerEvent('onMetaHeaders', array(&$this->metaHeaders));
}
@ -1501,7 +1626,7 @@ class Pico
$variables['%assets_url%'] = rtrim($this->getConfig('assets_url'), '/');
// replace %theme_url%
$variables['%theme_url%'] = $this->getConfig('themes_url') . $this->getConfig('theme');
$variables['%theme_url%'] = $this->getConfig('themes_url') . $this->getTheme();
// replace %meta.*%
if ($meta) {
@ -1964,7 +2089,7 @@ class Pico
if ($this->twig === null) {
$twigConfig = $this->getConfig('twig_config');
$twigLoader = new Twig_Loader_Filesystem($this->getThemesDir() . $this->getConfig('theme'));
$twigLoader = new Twig_Loader_Filesystem($this->getThemesDir() . $this->getTheme());
$this->twig = new Twig_Environment($twigLoader, $twigConfig);
$this->twig->addExtension(new PicoTwigExtension($this));
@ -2011,7 +2136,7 @@ class Pico
'plugins_url' => rtrim($this->getConfig('plugins_url'), '/'),
'themes_url' => rtrim($this->getConfig('themes_url'), '/'),
'assets_url' => rtrim($this->getConfig('assets_url'), '/'),
'theme_url' => $this->getConfig('themes_url') . $this->getConfig('theme'),
'theme_url' => $this->getConfig('themes_url') . $this->getTheme(),
'site_title' => $this->getConfig('site_title'),
'meta' => $this->meta,
'content' => $this->content,

View file

@ -113,6 +113,35 @@ class DummyPlugin extends AbstractPicoPlugin
// your code
}
/**
* Triggered before Pico loads its theme
*
* @see Pico::loadTheme()
* @see DummyPlugin::onThemeLoaded()
*
* @param string $theme name of current theme
*/
public function onThemeLoading(&$theme)
{
// your code
}
/**
* Triggered after Pico loaded its theme
*
* @see DummyPlugin::onThemeLoading()
* @see Pico::getTheme()
* @see Pico::getThemeApiVersion()
*
* @param string $theme name of current theme
* @param int $themeApiVersion API version of the theme
* @param array $themeConfig config array of the theme
*/
public function onThemeLoaded($theme, $themeApiVersion, array &$themeConfig)
{
// your code
}
/**
* Triggered after Pico has evaluated the request URL
*