diff --git a/lib/outputlib.php b/lib/outputlib.php index dc40d2bdc65..0d130113ecd 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -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 @@ -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 $scssfile = false; + public $scss = false; + + /** + * Local cache of the SCSS property. + * @var false|array + */ + 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. * diff --git a/theme/boost/config.php b/theme/boost/config.php index c8e08c112fb..d5794fc8ea5 100644 --- a/theme/boost/config.php +++ b/theme/boost/config.php @@ -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. diff --git a/theme/boost/lib.php b/theme/boost/lib.php index cc12f196795..e662f64c939 100644 --- a/theme/boost/lib.php +++ b/theme/boost/lib.php @@ -46,38 +46,16 @@ function theme_boost_get_extra_scss($theme) { } /** - * Get SCSS to prepend. + * Returns the main SCSS content. * * @param theme_config $theme The theme config object. - * @return array + * @return string */ -function theme_boost_get_pre_scss($theme) { +function theme_boost_get_main_scss_content($theme) { global $CFG; $scss = ''; - $configurable = [ - // Config key => [variableName, ...]. - 'brandcolor' => ['brand-primary'], - ]; - - // Prepend variables first. - foreach ($configurable as $configkey => $targets) { - $value = $theme->settings->{$configkey}; - if (empty($value)) { - continue; - } - array_map(function($target) use (&$scss, $value) { - $scss .= '$' . $target . ': ' . $value . ";\n"; - }, (array) $targets); - } - - // Prepend pre-scss. - if (!empty($theme->settings->scsspre)) { - $scss .= $theme->settings->scsspre; - } - - // Now append the preset. - $filename = $theme->settings->preset; + $filename = !empty($theme->settings->preset) ? $theme->settings->preset : null; $fs = get_file_storage(); $context = context_system::instance(); @@ -94,3 +72,37 @@ function theme_boost_get_pre_scss($theme) { return $scss; } + +/** + * Get SCSS to prepend. + * + * @param theme_config $theme The theme config object. + * @return array + */ +function theme_boost_get_pre_scss($theme) { + global $CFG; + + $scss = ''; + $configurable = [ + // Config key => [variableName, ...]. + 'brandcolor' => ['brand-primary'], + ]; + + // Prepend variables first. + foreach ($configurable as $configkey => $targets) { + $value = isset($theme->settings->{$configkey}) ? $theme->settings->{$configkey} : null; + if (empty($value)) { + continue; + } + array_map(function($target) use (&$scss, $value) { + $scss .= '$' . $target . ': ' . $value . ";\n"; + }, (array) $targets); + } + + // Prepend pre-scss. + if (!empty($theme->settings->scsspre)) { + $scss .= $theme->settings->scsspre; + } + + return $scss; +} diff --git a/theme/upgrade.txt b/theme/upgrade.txt index 901e44d2641..9a17dd608b5 100644 --- a/theme/upgrade.txt +++ b/theme/upgrade.txt @@ -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