2 namespace wcf\system\style
;
3 use Leafo\ScssPhp\Compiler
;
4 use wcf\data\application\Application
;
5 use wcf\data\option\Option
;
6 use wcf\data\style\Style
;
7 use wcf\system\application\ApplicationHandler
;
8 use wcf\system\event\EventHandler
;
9 use wcf\system\exception\SystemException
;
10 use wcf\system\SingletonFactory
;
12 use wcf\util\FileUtil
;
13 use wcf\util\StringUtil
;
14 use wcf\util\StyleUtil
;
17 * Provides access to the SCSS PHP compiler.
19 * @author Alexander Ebert
20 * @copyright 2001-2019 WoltLab GmbH
21 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 * @package WoltLabSuite\Core\System\Style
24 class StyleCompiler
extends SingletonFactory
{
26 * SCSS compiler object
27 * @var \Leafo\ScssPhp\Compiler
29 protected $compiler = null;
32 * names of option types which are supported as additional variables
35 public static $supportedOptionType = ['boolean', 'float', 'integer', 'radioButton', 'select'];
38 * file used to store global SCSS declarations, relative to `WCF_DIR`
41 const FILE_GLOBAL_VALUES
= 'style/ui/zzz_wsc_style_global_values.scss';
44 * registry keys for data storage
47 const REGISTRY_GLOBAL_VALUES
= 'styleGlobalValues';
52 protected function init() {
53 require_once(WCF_DIR
.'lib/system/style/scssphp/scss.inc.php');
54 $this->compiler
= new Compiler();
55 // Disable Unicode support because of its horrible performance (7x slowdown)
56 // https://github.com/WoltLab/WCF/pull/2736#issuecomment-416084079
57 $this->compiler
->setEncoding('iso8859-1');
58 $this->compiler
->setImportPaths([WCF_DIR
]);
62 * Compiles SCSS stylesheets.
66 public function compile(Style
$style) {
67 $files = $this->getCoreFiles();
69 // read stylesheets in dependency order
70 $sql = "SELECT filename, application
71 FROM wcf".WCF_N
."_package_installation_file_log
72 WHERE CONVERT(filename using utf8) REGEXP ?
75 $statement = WCF
::getDB()->prepareStatement($sql);
77 'style/([a-zA-Z0-9\-\.]+)\.scss',
80 while ($row = $statement->fetchArray()) {
81 // the global values will always be evaluated last
82 if ($row['filename'] === self
::FILE_GLOBAL_VALUES
) {
86 $files[] = Application
::getDirectory($row['application']).$row['filename'];
90 if (file_exists(WCF_DIR
. self
::FILE_GLOBAL_VALUES
)) {
91 $files[] = WCF_DIR
. self
::FILE_GLOBAL_VALUES
;
94 // get style variables
95 $variables = $style->getVariables();
97 if (isset($variables['individualScss'])) {
98 $individualScss = $variables['individualScss'];
99 unset($variables['individualScss']);
102 // add style image path
103 $imagePath = '../images/';
104 if ($style->imagePath
) {
105 $imagePath = FileUtil
::getRelativePath(WCF_DIR
. 'style/', WCF_DIR
. $style->imagePath
);
106 $imagePath = FileUtil
::addTrailingSlash(FileUtil
::unifyDirSeparator($imagePath));
108 $variables['style_image_path'] = "'{$imagePath}'";
111 if (isset($variables['overrideScss'])) {
112 $lines = explode("\n", StringUtil
::unifyNewlines($variables['overrideScss']));
113 foreach ($lines as $line) {
114 if (preg_match('~^@([a-zA-Z]+): ?([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$~', $line, $matches)) {
115 $variables[$matches[1]] = $matches[2];
118 unset($variables['overrideScss']);
122 $variables['apiVersion'] = $style->apiVersion
;
124 $parameters = ['scss' => ''];
125 EventHandler
::getInstance()->fireAction($this, 'compile', $parameters);
127 $this->compileStylesheet(
128 WCF_DIR
.'style/style-'.$style->styleID
,
131 $individualScss . (!empty($parameters['scss']) ?
"\n" . $parameters['scss'] : ''),
132 function($content) use ($style) {
133 return "/* stylesheet for '".$style->styleName
."', generated on ".gmdate('r')." -- DO NOT EDIT */\n\n" . $content;
139 * Compiles SCSS stylesheets for ACP usage.
141 public function compileACP() {
142 if (substr(WCF_VERSION
, 0, 3) == '2.1') {
143 // work-around for wcf2.1 update
147 $files = $this->getCoreFiles();
149 // ACP uses a slightly different layout
150 $files[] = WCF_DIR
. 'acp/style/layout.scss';
152 // include stylesheets from other apps in arbitrary order
154 foreach (ApplicationHandler
::getInstance()->getApplications() as $application) {
155 $files = array_merge($files, $this->getAcpStylesheets($application));
159 // read default values
160 $sql = "SELECT variableName, defaultValue
161 FROM wcf".WCF_N
."_style_variable
162 ORDER BY variableID ASC";
163 $statement = WCF
::getDB()->prepareStatement($sql);
164 $statement->execute();
166 while ($row = $statement->fetchArray()) {
167 $value = $row['defaultValue'];
172 $variables[$row['variableName']] = $value;
175 $variables['wcfFontFamily'] = $variables['wcfFontFamilyFallback'];
176 if (!empty($variables['wcfFontFamilyGoogle'])) {
177 $variables['wcfFontFamily'] = '"' . $variables['wcfFontFamilyGoogle'] . '", ' . $variables['wcfFontFamily'];
180 $variables['style_image_path'] = "'../images/'";
182 $this->compileStylesheet(
183 WCF_DIR
.'acp/style/style',
188 // fix relative paths
189 $content = str_replace('../font/', '../../font/', $content);
190 $content = str_replace('../icon/', '../../icon/', $content);
191 $content = preg_replace('~\.\./images/~', '../../images/', $content);
193 return "/* stylesheet for ACP, generated on ".gmdate('r')." -- DO NOT EDIT */\n\n" . $content;
199 * Returns a list of common stylesheets provided by the core.
201 * @return string[] list of common stylesheets
203 protected function getCoreFiles() {
205 if ($handle = opendir(WCF_DIR
.'style/')) {
206 while (($file = readdir($handle)) !== false) {
207 if ($file === '.' ||
$file === '..' ||
$file === 'bootstrap' ||
is_file(WCF_DIR
.'style/'.$file)) {
211 $file = WCF_DIR
."style/{$file}/";
212 if ($innerHandle = opendir($file)) {
213 while (($innerFile = readdir($innerHandle)) !== false) {
214 if ($innerFile === '.' ||
$innerFile === '..' ||
!is_file($file.$innerFile) ||
!preg_match('~^[a-zA-Z0-9\-\.]+\.scss$~', $innerFile)) {
218 $files[] = $file.$innerFile;
220 closedir($innerHandle);
226 // directory order is not deterministic in some cases
234 * Returns the list of SCSS stylesheets of an application.
236 * @param Application $application
239 protected function getAcpStylesheets(Application
$application) {
240 if ($application->packageID
== 1) return [];
244 $basePath = FileUtil
::addTrailingSlash(FileUtil
::getRealPath(WCF_DIR
. $application->getPackage()->packageDir
)) . 'acp/style/';
245 $result = glob($basePath . '*.scss');
246 if (is_array($result)) {
247 foreach ($result as $file) {
256 * Prepares the style compiler by adding variables to environment.
258 * @param string[] $variables
261 protected function bootstrap(array $variables) {
262 // add reset like a boss
263 $content = $this->prepareFile(WCF_DIR
.'style/bootstrap/reset.scss');
265 // apply style variables
266 $this->compiler
->setVariables($variables);
269 $content .= $this->prepareFile(WCF_DIR
.'style/bootstrap/mixin.scss');
271 // add newer mixins added with version 3.0
272 foreach (glob(WCF_DIR
.'style/bootstrap/mixin/*.scss') as $mixin) {
273 $content .= $this->prepareFile($mixin);
280 * Prepares a SCSS stylesheet for importing.
282 * @param string $filename
284 * @throws SystemException
286 protected function prepareFile($filename) {
287 if (!file_exists($filename) ||
!is_readable($filename)) {
288 throw new SystemException("Unable to access '".$filename."', does not exist or is not readable");
291 // use a relative path
292 $filename = FileUtil
::getRelativePath(WCF_DIR
, dirname($filename)) . basename($filename);
293 return '@import "'.$filename.'";'."\n";
297 * Compiles SCSS stylesheets into one CSS-stylesheet and writes them
298 * to filesystem. Please be aware not to append '.css' within $filename!
300 * @param string $filename
301 * @param string[] $files
302 * @param string[] $variables
303 * @param string $individualScss
304 * @param callable $callback
305 * @throws SystemException
307 protected function compileStylesheet($filename, array $files, array $variables, $individualScss, callable
$callback) {
308 foreach ($variables as &$value) {
309 if (StringUtil
::startsWith($value, '../')) {
310 $value = '~"'.$value.'"';
315 $variables['wcfFontFamily'] = $variables['wcfFontFamilyFallback'];
316 if (!empty($variables['wcfFontFamilyGoogle'])) {
317 // The SCSS parser attempts to evaluate the variables, causing issues with font names that
318 // include logical operators such as "And" or "Or".
319 $variables['wcfFontFamilyGoogle'] = '"' . $variables['wcfFontFamilyGoogle'] . '"';
321 $variables['wcfFontFamily'] = $variables['wcfFontFamilyGoogle'] . ', ' . $variables['wcfFontFamily'];
324 // add options as SCSS variables
326 foreach (Option
::getOptions() as $constantName => $option) {
327 if (in_array($option->optionType
, static::$supportedOptionType)) {
328 $variables['wcf_option_'.mb_strtolower($constantName)] = is_int($option->optionValue
) ?
$option->optionValue
: '"'.$option->optionValue
.'"';
333 if (!isset($variables['apiVersion'])) $variables['apiVersion'] = Style
::API_VERSION
;
336 // workaround during setup
337 $variables['wcf_option_attachment_thumbnail_height'] = '~"210"';
338 $variables['wcf_option_attachment_thumbnail_width'] = '~"280"';
339 $variables['wcf_option_signature_max_image_height'] = '~"150"';
341 $variables['apiVersion'] = Style
::API_VERSION
;
344 // convert into numeric value for comparison, e.g. `3.1` -> `31`
345 $variables['apiVersion'] = str_replace('.', '', $variables['apiVersion']);
347 // build SCSS bootstrap
348 $scss = $this->bootstrap($variables);
349 foreach ($files as $file) {
350 $scss .= $this->prepareFile($file);
353 // append individual CSS/SCSS
354 if ($individualScss) {
355 $scss .= $individualScss;
359 $this->compiler
->setFormatter('Leafo\ScssPhp\Formatter\Crunched');
360 $content = $this->compiler
->compile($scss);
362 catch (\Exception
$e) {
363 throw new SystemException("Could not compile SCSS: ".$e->getMessage(), 0, '', $e);
366 $content = $callback($content);
369 file_put_contents($filename.'.css', $content);
370 FileUtil
::makeWritable($filename.'.css');
372 // convert stylesheet to RTL
373 $content = StyleUtil
::convertCSSToRTL($content);
375 // force code boxes to be always LTR
376 $content .= "\n/* RTL fix for code boxes */\n";
377 $content .= '.codeBox > div > ol > li > span:last-child, .redactor-layer pre { direction: ltr; text-align: left; } .codeBox > div > ol > li > span:last-child { display: block; }';
379 // write stylesheet for RTL
380 file_put_contents($filename.'-rtl.css', $content);
381 FileUtil
::makeWritable($filename.'-rtl.css');