Bladeren bron

:tada: 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
Daniel Rudolf 5 jaren geleden
bovenliggende
commit
b27b4f388a
3 gewijzigde bestanden met toevoegingen van 180 en 25 verwijderingen
  1. 2 1
      config/config.yml.template
  2. 149 24
      lib/Pico.php
  3. 29 0
      plugins/DummyPlugin.php

+ 2 - 1
config/config.yml.template

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

+ 149 - 24
lib/Pico.php

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

+ 29 - 0
plugins/DummyPlugin.php

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