Merge branch 'MDL-56959-master' of git://github.com/FMCorz/moodle
authorDan Poltawski <dan@moodle.com>
Thu, 17 Nov 2016 18:01:58 +0000 (18:01 +0000)
committerDan Poltawski <dan@moodle.com>
Thu, 17 Nov 2016 18:01:58 +0000 (18:01 +0000)
lib/outputlib.php
theme/boost/config.php
theme/boost/lib.php
theme/upgrade.txt

index 4e5e58e..7232a65 100644 (file)
@@ -152,6 +152,9 @@ class theme_config {
      */
     const DEFAULT_THEME = 'boost';
 
+    /** The key under which the SCSS file is stored amongst the CSS files. */
+    const SCSS_KEY = '__SCSS__';
+
     /**
      * @var array You can base your theme on other themes by linking to the other theme as
      * parents. This lets you use the CSS and layouts from the other themes
@@ -294,7 +297,7 @@ class theme_config {
      * object as second parameter. A return value is not required, the tree can
      * be edited in place.
      */
-    public $csstreepostprocess = null;
+    public $csstreepostprocessor = null;
 
     /**
      * @var string Accessibility: Right arrow-like character is
@@ -427,10 +430,18 @@ class theme_config {
     public $lessfile = false;
 
     /**
-     * The SCSS file to compile. This takes precedence over the LESS file.
-     * @var string
+     * The SCSS file to compile (without .scss), located in the scss/ folder of the theme.
+     * Or a Closure, which receives the theme_config as argument and must
+     * return the SCSS content. This setting takes precedence over self::$lessfile.
+     * @var string|Closure
+     */
+    public $scss = false;
+
+    /**
+     * Local cache of the SCSS property.
+     * @var false|array
      */
-    public $scssfile = false;
+    protected $scsscache = null;
 
     /**
      * The name of the function to call to get the LESS code to inject.
@@ -535,7 +546,7 @@ class theme_config {
             'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
             'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
             'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
-            'scssfile', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor');
+            'scss', 'extrascsscallback', 'prescsscallback', 'csstreepostprocessor');
 
         foreach ($config as $key=>$value) {
             if (in_array($key, $configurable)) {
@@ -815,7 +826,7 @@ class theme_config {
                     // We need to serve parents individually otherwise we may easily exceed the style limit IE imposes (4096).
                     $urls[] = new moodle_url($baseurl, array('theme'=>$this->name,'type'=>'ie', 'subtype'=>'parents', 'sheet'=>$parent));
                 }
-                if (!empty($this->scssfile)) {
+                if ($this->get_scss_property()) {
                     // No need to define the type as IE here.
                     $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
                 } else if (!empty($this->lessfile)) {
@@ -834,7 +845,7 @@ class theme_config {
                     }
                 }
                 foreach ($css['theme'] as $sheet => $filename) {
-                    if ($sheet === $this->scssfile) {
+                    if ($sheet === self::SCSS_KEY) {
                         // This is the theme SCSS file.
                         $urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
                     } else if ($sheet === $this->lessfile) {
@@ -868,7 +879,7 @@ class theme_config {
                         $csscontent .= file_get_contents($v) . "\n";
                     }
                 } else {
-                    if ($type === 'theme' && $identifier === $this->scssfile) {
+                    if ($type === 'theme' && $identifier === self::SCSS_KEY) {
                         // We need the content from SCSS because this is the SCSS file from the theme.
                         $csscontent .= $this->get_css_content_from_scss(false);
                     } else if ($type === 'theme' && $identifier === $this->lessfile) {
@@ -935,7 +946,7 @@ class theme_config {
             } else if ($subtype === 'theme') {
                 $cssfiles = $css['theme'];
                 foreach ($cssfiles as $key => $value) {
-                    if (in_array($key, [$this->lessfile, $this->scssfile])) {
+                    if (in_array($key, [$this->lessfile, self::SCSS_KEY])) {
                         // Remove the LESS/SCSS file from the theme CSS files.
                         // The LESS/SCSS files use the type 'less' or 'scss', not 'ie'.
                         unset($cssfiles[$key]);
@@ -1075,15 +1086,13 @@ class theme_config {
             }
         }
 
+
         // Current theme sheets and less file.
         // We first add the SCSS, or LESS file because we want the CSS ones to
-        // be included after the SCSS/LESS code. However, if both the SCSS/LESS file
+        // be included after the SCSS/LESS code. However, if both the LESS file
         // and a CSS file share the same name, the CSS file is ignored.
-        if (!empty($this->scssfile)) {
-            $sheetfile = "{$this->dir}/scss/{$this->scssfile}.scss";
-            if (is_readable($sheetfile)) {
-                $cssfiles['theme'][$this->scssfile] = $sheetfile;
-            }
+        if ($this->get_scss_property()) {
+            $cssfiles['theme'][self::SCSS_KEY] = true;
         } else if (!empty($this->lessfile)) {
             $sheetfile = "{$this->dir}/less/{$this->lessfile}.less";
             if (is_readable($sheetfile)) {
@@ -1181,24 +1190,23 @@ class theme_config {
     protected function get_css_content_from_scss($themedesigner) {
         global $CFG;
 
-        $scssfile = $this->scssfile;
-        if (!$scssfile || !is_readable($this->dir . '/scss/' . $scssfile . '.scss')) {
+        list($paths, $scss) = $this->get_scss_property();
+        if (!$scss) {
             throw new coding_exception('The theme did not define a SCSS file, or it is not readable.');
         }
 
         // We might need more memory to do this, so let's play safe.
         raise_memory_limit(MEMORY_EXTRA);
 
-        // Files list.
-        $files = $this->get_css_files($themedesigner);
-
-        // Get the SCSS file path.
-        $themescssfile = $files['theme'][$scssfile];
-
         // Set-up the compiler.
         $compiler = new core_scss();
         $compiler->prepend_raw_scss($this->get_pre_scss_code());
-        $compiler->set_file($themescssfile);
+        if (is_string($scss)) {
+            $compiler->set_file($scss);
+        } else {
+            $compiler->append_raw_scss($scss($this));
+            $compiler->setImportPaths($paths);
+        }
         $compiler->append_raw_scss($this->get_extra_scss_code());
 
         try {
@@ -1207,7 +1215,7 @@ class theme_config {
 
         } catch (\Leafo\ScssPhp\Exception $e) {
             $compiled = false;
-            debugging('Error while compiling SCSS ' . $scssfile . ' file: ' . $e->getMessage(), DEBUG_DEVELOPER);
+            debugging('Error while compiling SCSS: ' . $e->getMessage(), DEBUG_DEVELOPER);
         }
 
         // Try to save memory.
@@ -1344,6 +1352,47 @@ class theme_config {
         return $content;
     }
 
+    /**
+     * Get the SCSS property.
+     *
+     * This resolves whether a SCSS file (or content) has to be used when generating
+     * the stylesheet for the theme. It will look at parents themes and check the
+     * SCSS properties there.
+     *
+     * @return False when SCSS is not used.
+     *         An array with the import paths, and the path to the SCSS file or Closure as second.
+     */
+    public function get_scss_property() {
+        if ($this->scsscache === null) {
+            $configs = [$this] + $this->parent_configs;
+            $scss = null;
+
+            foreach ($configs as $config) {
+                $path = "{$config->dir}/scss";
+
+                // We collect the SCSS property until we've found one.
+                if (empty($scss) && !empty($config->scss)) {
+                    $candidate = is_string($config->scss) ? "{$path}/{$config->scss}.scss" : $config->scss;
+                    if ($candidate instanceof Closure) {
+                        $scss = $candidate;
+                    } else if (is_string($candidate) && is_readable($candidate)) {
+                        $scss = $candidate;
+                    }
+                }
+
+                // We collect the import paths once we've found a SCSS property.
+                if ($scss && is_dir($path)) {
+                    $paths[] = $path;
+                }
+
+            }
+
+            $this->scsscache = $scss !== null ? [$paths, $scss] : false;
+        }
+
+        return $this->scsscache;
+    }
+
     /**
      * Generate a URL to the file that serves theme JavaScript files.
      *
@@ -1521,8 +1570,8 @@ class theme_config {
         }
 
         // Post processing using an object representation of CSS.
-        $hastreeprocessor = !empty($this->csstreepostprocessor) && function_exists($this->csstreepostprocessor);
-        $needsparsing = $hastreeprocessor || !empty($this->rtlmode);
+        $treeprocessor = $this->get_css_tree_post_processor();
+        $needsparsing = !empty($treeprocessor) || !empty($this->rtlmode);
         if ($needsparsing) {
             $parser = new core_cssparser($css);
             $csstree = $parser->parse();
@@ -1532,9 +1581,8 @@ class theme_config {
                 $this->rtlize($csstree);
             }
 
-            if ($hastreeprocessor) {
-                $fn = $this->csstreepostprocessor;
-                $fn($csstree, $this);
+            if ($treeprocessor) {
+                $treeprocessor($csstree, $this);
             }
 
             $css = $csstree->render();
@@ -2175,6 +2223,21 @@ class theme_config {
         // Default it to blocks.
         return 'blocks';
     }
+
+    /**
+     * Get the callable for CSS tree post processing.
+     *
+     * @return string|null
+     */
+    public function get_css_tree_post_processor() {
+        $configs = [$this] + $this->parent_configs;
+        foreach ($configs as $config) {
+            if ($config->csstreepostprocessor && is_callable($config->csstreepostprocessor)) {
+                return $config->csstreepostprocessor;
+            }
+        }
+        return null;
+    }
 }
 
 /**
index c8e08c1..d5794fc 100644 (file)
@@ -27,9 +27,11 @@ defined('MOODLE_INTERNAL') || die();
 require_once(__DIR__ . '/lib.php');
 
 $THEME->name = 'boost';
-$THEME->scssfile = 'preset';
 $THEME->sheets = [];
 $THEME->editor_sheets = [];
+$THEME->scss = function($theme) {
+    return theme_boost_get_main_scss_content($theme);
+};
 
 $THEME->layouts = [
     // Most backwards compatible layout without the blocks - this is the layout used by default.
index cc12f19..e662f64 100644 (file)
@@ -45,6 +45,34 @@ function theme_boost_get_extra_scss($theme) {
     return !empty($theme->settings->scss) ? $theme->settings->scss : '';
 }
 
+/**
+ * Returns the main SCSS content.
+ *
+ * @param theme_config $theme The theme config object.
+ * @return string
+ */
+function theme_boost_get_main_scss_content($theme) {
+    global $CFG;
+
+    $scss = '';
+    $filename = !empty($theme->settings->preset) ? $theme->settings->preset : null;
+    $fs = get_file_storage();
+
+    $context = context_system::instance();
+    if ($filename == 'default.scss') {
+        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');
+    } else if ($filename == 'plain.scss') {
+        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/plain.scss');
+    } else if ($filename && ($presetfile = $fs->get_file($context->id, 'theme_boost', 'preset', 0, '/', $filename))) {
+        $scss .= $presetfile->get_content();
+    } else {
+        // Safety fallback - maybe new installs etc.
+        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');
+    }
+
+    return $scss;
+}
+
 /**
  * Get SCSS to prepend.
  *
@@ -62,7 +90,7 @@ function theme_boost_get_pre_scss($theme) {
 
     // Prepend variables first.
     foreach ($configurable as $configkey => $targets) {
-        $value = $theme->settings->{$configkey};
+        $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null;
         if (empty($value)) {
             continue;
         }
@@ -76,21 +104,5 @@ function theme_boost_get_pre_scss($theme) {
         $scss .= $theme->settings->scsspre;
     }
 
-    // Now append the preset.
-    $filename = $theme->settings->preset;
-    $fs = get_file_storage();
-
-    $context = context_system::instance();
-    if ($filename == 'default.scss') {
-        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');
-    } else if ($filename == 'plain.scss') {
-        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/plain.scss');
-    } else if ($filename && ($presetfile = $fs->get_file($context->id, 'theme_boost', 'preset', 0, '/', $filename))) {
-        $scss .= $presetfile->get_content();
-    } else {
-        // Safety fallback - maybe new installs etc.
-        $scss .= file_get_contents($CFG->dirroot . '/theme/boost/scss/preset/default.scss');
-    }
-
     return $scss;
 }
index 901e44d..9a17dd6 100644 (file)
@@ -19,9 +19,10 @@ Removed themes:
  * CLI svgtool.php has moved from theme/base/cli to admin/cli and paths should be relative to the new location.
 * Bootstrap 4 was added as part of a the new theme 'boost'.
 * Themes can now automatically compile SCSS on the fly. This works the same way as it
-  does compiling LESS on the fly, effecitvely adding $THEME->scssfile to your config.
+  does compiling LESS on the fly, effecitvely adding $THEME->scss to your config.
 * Two new callbacks allow themes to inject SCSS code before and after the content provided
-  by the SCSS file $THEME->scssfile. See $THEME->prescsscallback and $THEME->extrascsscallback.
+  by the SCSS file $THEME->scss. See $THEME->prescsscallback and $THEME->extrascsscallback.
+* $THEME->scss can also be a Closure which will return the main SCSS content.
 * Using .dir-rtl for RTL styling is deprecated and should not be used any more. From now
   the styles are automatically flipped when the language is right-to-left. However,
   as this is not always perfect, you can define exceptions. Please refer to the documentation