MDL-55286 theme: Support compiling SCSS on-demand from a theme

Part of MDL-55071
This commit is contained in:
Frederic Massart 2016-07-20 17:44:14 +08:00 committed by Dan Poltawski
parent 321c986c86
commit 65b8336ed3
7 changed files with 292 additions and 15115 deletions

114
lib/classes/scss.php Normal file
View File

@ -0,0 +1,114 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle implementation of SCSS.
*
* @package core
* @copyright 2016 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
// TODO MDL-53016 Remove this when the latter is implemented.
require_once($CFG->libdir . '/scssphp/Base/Range.php');
require_once($CFG->libdir . '/scssphp/Block.php');
require_once($CFG->libdir . '/scssphp/Colors.php');
require_once($CFG->libdir . '/scssphp/Compiler.php');
require_once($CFG->libdir . '/scssphp/Compiler/Environment.php');
require_once($CFG->libdir . '/scssphp/Exception/CompilerException.php');
require_once($CFG->libdir . '/scssphp/Exception/ParserException.php');
require_once($CFG->libdir . '/scssphp/Exception/ServerException.php');
require_once($CFG->libdir . '/scssphp/Formatter.php');
require_once($CFG->libdir . '/scssphp/Formatter/Compact.php');
require_once($CFG->libdir . '/scssphp/Formatter/Compressed.php');
require_once($CFG->libdir . '/scssphp/Formatter/Crunched.php');
require_once($CFG->libdir . '/scssphp/Formatter/Debug.php');
require_once($CFG->libdir . '/scssphp/Formatter/Expanded.php');
require_once($CFG->libdir . '/scssphp/Formatter/Nested.php');
require_once($CFG->libdir . '/scssphp/Formatter/OutputBlock.php');
require_once($CFG->libdir . '/scssphp/Node.php');
require_once($CFG->libdir . '/scssphp/Node/Number.php');
require_once($CFG->libdir . '/scssphp/Parser.php');
require_once($CFG->libdir . '/scssphp/Type.php');
require_once($CFG->libdir . '/scssphp/Util.php');
require_once($CFG->libdir . '/scssphp/Version.php');
require_once($CFG->libdir . '/scssphp/Server.php');
/**
* Moodle SCSS compiler class.
*
* @package core
* @copyright 2016 Frédéric Massart
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_scss extends \Leafo\ScssPhp\Compiler {
/** @var string The path to the SCSS file. */
protected $scssfile;
/** @var array Bits of SCSS content. */
protected $scsscontent = array();
/**
* Add variables.
*
* @param array $scss Associative array of variables and their values.
* @return void
*/
public function add_variables(array $variables) {
$this->setVariables($variables);
}
/**
* Append raw SCSS to what's to compile.
*
* @param string $scss SCSS code.
* @return void
*/
public function append_raw_scss($scss) {
$this->scsscontent[] = $scss;
}
/**
* Set the file to compile from.
*
* The purpose of this method is to provide a way to import the
* content of a file without messing with the import directories.
*
* @param string $filepath The path to the file.
* @return void
*/
public function set_file($filepath) {
$this->scssfile = $filepath;
$this->setImportPaths([dirname($filepath)]);
}
/**
* Compiles to CSS.
*
* @return string
*/
public function to_css() {
$content = '';
if (!empty($this->scssfile)) {
$content .= file_get_contents($this->scssfile);
}
$content .= implode(';', $this->scsscontent);
return $this->compile($content);
}
}

View File

@ -410,18 +410,36 @@ class theme_config {
*/
public $lessfile = false;
/**
* The SCSS file to compile. This takes precedence over the LESS file.
* @var string
*/
public $scssfile = false;
/**
* The name of the function to call to get the LESS code to inject.
* @var string
*/
public $extralesscallback = null;
/**
* The name of the function to call to get the SCSS code to inject.
* @var string
*/
public $extrascsscallback = null;
/**
* The name of the function to call to get extra LESS variables.
* @var string
*/
public $lessvariablescallback = null;
/**
* The name of the function to call to get extra SCSS variables.
* @var string
*/
public $scssvariablescallback = null;
/**
* Sets the render method that should be used for rendering custom block regions by scripts such as my/index.php
* Defaults to {@link core_renderer::blocks_for_region()}
@ -500,7 +518,8 @@ class theme_config {
'layouts', 'enable_dock', 'enablecourseajax', 'supportscssoptimisation',
'rendererfactory', 'csspostprocess', 'editor_sheets', 'rarrow', 'larrow', 'uarrow', 'darrow',
'hidefromselector', 'doctype', 'yuicssmodules', 'blockrtlmanipulations',
'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod');
'lessfile', 'extralesscallback', 'lessvariablescallback', 'blockrendermethod',
'scssfile', 'extrascsscallback', 'scssvariablescallback');
foreach ($config as $key=>$value) {
if (in_array($key, $configurable)) {
@ -776,7 +795,10 @@ 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->lessfile)) {
if (!empty($this->scssfile)) {
// 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)) {
// No need to define the type as IE here.
$urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
}
@ -792,7 +814,10 @@ class theme_config {
}
}
foreach ($css['theme'] as $sheet => $filename) {
if ($sheet === $this->lessfile) {
if ($sheet === $this->scssfile) {
// This is the theme SCSS file.
$urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'scss'));
} else if ($sheet === $this->lessfile) {
// This is the theme LESS file.
$urls[] = new moodle_url($baseurl, array('theme' => $this->name, 'type' => 'less'));
} else {
@ -825,7 +850,10 @@ class theme_config {
$csscontent .= file_get_contents($v) . "\n";
}
} else {
if ($type === 'theme' && $identifier === $this->lessfile) {
if ($type === 'theme' && $identifier === $this->scssfile) {
// 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) {
// We need the content from LESS because this is the LESS file from the theme.
$csscontent .= $this->get_css_content_from_less(false);
} else {
@ -867,8 +895,15 @@ class theme_config {
global $CFG;
require_once($CFG->dirroot.'/lib/csslib.php');
// The LESS file of the theme is requested.
if ($type === 'less') {
if ($type === 'scss') {
// The SCSS file of the theme is requested.
$csscontent = $this->get_css_content_from_scss(true);
if ($csscontent !== false) {
return $csscontent;
}
return '';
} else if ($type === 'less') {
// The LESS file of the theme is requested.
$csscontent = $this->get_css_content_from_less(true);
if ($csscontent !== false) {
return $csscontent;
@ -906,9 +941,9 @@ class theme_config {
} else if ($subtype === 'theme') {
$cssfiles = $css['theme'];
foreach ($cssfiles as $key => $value) {
if ($this->lessfile && $key === $this->lessfile) {
// Remove the LESS file from the theme CSS files.
// The LESS files use the type 'less', not 'ie'.
if (in_array($key, [$this->lessfile, $this->scssfile])) {
// 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]);
}
}
@ -1053,10 +1088,15 @@ class theme_config {
}
// Current theme sheets and less file.
// We first add the LESS files because we want the CSS ones to be included after the
// LESS code. However, if both the LESS file and the CSS file share the same name,
// the CSS file is ignored.
if (!empty($this->lessfile)) {
// 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
// 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;
}
} else if (!empty($this->lessfile)) {
$sheetfile = "{$this->dir}/less/{$this->lessfile}.less";
if (is_readable($sheetfile)) {
$cssfiles['theme'][$this->lessfile] = $sheetfile;
@ -1146,6 +1186,52 @@ class theme_config {
return $compiled;
}
/**
* Return the CSS content generated from the SCSS file.
*
* @param bool $themedesigner True if theme designer is enabled.
* @return bool|string Return false when the compilation failed. Else the compiled string.
*/
protected function get_css_content_from_scss($themedesigner) {
global $CFG;
$scssfile = $this->scssfile;
if (!$scssfile || !is_readable($this->dir . '/scss/' . $scssfile . '.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->set_file($themescssfile);
$compiler->append_raw_scss($this->get_extra_scss_code());
$compiler->add_variables($this->get_scss_variables());
try {
// Compile!
$compiled = $compiler->to_css();
$compiled = $this->post_process($compiled);
} catch (\Leafo\ScssPhp\Exception $e) {
$compiled = false;
debugging('Error while compiling SCSS ' . $scssfile . ' file: ' . $e->getMessage(), DEBUG_DEVELOPER);
}
// Try to save memory.
$compiler = null;
unset($compiler);
return $compiled;
}
/**
* Return extra LESS variables to use when compiling.
*
@ -1179,6 +1265,39 @@ class theme_config {
return $variables;
}
/**
* Return extra SCSS variables to use when compiling.
*
* @return array Where keys are the variable names, and the values are the value.
*/
protected function get_scss_variables() {
$variables = array();
// Getting all the candidate functions.
$candidates = array();
foreach ($this->parent_configs as $parent_config) {
if (!isset($parent_config->scssvariablescallback)) {
continue;
}
$candidates[] = $parent_config->scssvariablescallback;
}
$candidates[] = $this->scssvariablescallback;
// Calling the functions.
foreach ($candidates as $function) {
if (function_exists($function)) {
$vars = $function($this);
if (!is_array($vars)) {
debugging('Callback ' . $function . ' did not return an array() as expected', DEBUG_DEVELOPER);
continue;
}
$variables = array_merge($variables, $vars);
}
}
return $variables;
}
/**
* Return extra LESS code to add when compiling.
*
@ -1211,6 +1330,38 @@ class theme_config {
return $content;
}
/**
* Return extra SCSS code to add when compiling.
*
* This is intended to be used by themes to inject some SCSS code
* before it gets compiled. If you want to inject variables you
* should use {@link self::get_scss_variables()}.
*
* @return string The SCSS code to inject.
*/
protected function get_extra_scss_code() {
$content = '';
// Getting all the candidate functions.
$candidates = array();
foreach ($this->parent_configs as $parent_config) {
if (!isset($parent_config->extrascsscallback)) {
continue;
}
$candidates[] = $parent_config->extrascsscallback;
}
$candidates[] = $this->extrascsscallback;
// Calling the functions.
foreach ($candidates as $function) {
if (function_exists($function)) {
$content .= "\n/** Extra SCSS from $function **/\n" . $function($this) . "\n";
}
}
return $content;
}
/**
* Generate a URL to the file that serves theme JavaScript files.
*

View File

@ -25,7 +25,8 @@
defined('MOODLE_INTERNAL') || die();
$THEME->name = 'noname';
$THEME->sheets = ['build'];
$THEME->scssfile = 'moodle';
$THEME->sheets = [];
$THEME->editor_sheets = ['editor'];
$THEME->layouts = [

View File

@ -16,13 +16,13 @@
.environmenttable {
.warn {
@extend .table-warning;
background-color: $state-warning-bg;
}
.error {
@extend .table-danger;
background-color: $state-danger-bg;
}
.ok {
@extend .table-success;
background-color: $state-success-bg;
}
}
@ -144,7 +144,7 @@ img.iconsmall {
}
#page-admin-roles-define .capdefault {
@extend .table-hover;
background-color: $table-bg-hover;
}
#page-filter-manage .backlink,
@ -517,7 +517,7 @@ img.iconsmall {
#plugins-control-panel {
.status-missing td {
@extend .table-warning;
background-color: $state-warning-bg;
}
.pluginname {
.displayname img.icon {
@ -544,7 +544,7 @@ img.iconsmall {
}
.uninstall {
a {
@extend .label-warning;
color: $state-danger-text;
}
}
.notes {
@ -603,7 +603,7 @@ img.iconsmall {
.status-missing, .status-downgrade {
td {
@extend .table-danger;
background-color: $state-danger-bg;
}
}
@ -674,12 +674,12 @@ img.iconsmall {
#plugins-check-page, #plugins-control-panel {
.pluginupdateinfo {
@extend .table-info;
background-color: $state-info-bg;
&.maturity50 {
@extend .table-danger;
background-color: $state-danger-bg;
}
&.maturity100, &.maturity150 {
@extend .table-warning;
background-color: $state-warning-bg;
}
padding: 5px;
margin: 10px 0;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -151,10 +151,10 @@ if ($type === 'editor') {
$lock = null;
// Lock system to prevent concurrent requests to compile LESS, which is really slow and CPU intensive.
// Lock system to prevent concurrent requests to compile LESS/SCSS, which is really slow and CPU intensive.
// Each client should wait for one to finish the compilation before starting a new compiling process.
// We only do this when the file will be cached...
if ($type === 'less' && $cache) {
if (in_array($type, ['less', 'scss']) && $cache) {
$lockfactory = \core\lock\lock_config::get_lock_factory('core_theme_get_css_content');
// We wait for the lock to be acquired, the timeout does not need to be strict here.
$lock = $lockfactory->get_lock($themename, rand(15, 30));