Commit | Line | Data |
---|---|---|
b4cbf821 AE |
1 | <?php |
2 | namespace wcf\system\style; | |
8e87bc50 | 3 | use Leafo\ScssPhp\Compiler; |
6012e566 | 4 | use wcf\data\application\Application; |
8e40fc29 | 5 | use wcf\data\option\Option; |
b4cbf821 | 6 | use wcf\data\style\Style; |
c7011512 | 7 | use wcf\system\application\ApplicationHandler; |
3fe11507 | 8 | use wcf\system\event\EventHandler; |
b4cbf821 AE |
9 | use wcf\system\exception\SystemException; |
10 | use wcf\system\SingletonFactory; | |
11 | use wcf\system\WCF; | |
12 | use wcf\util\FileUtil; | |
d409e4fa | 13 | use wcf\util\StringUtil; |
b4cbf821 AE |
14 | use wcf\util\StyleUtil; |
15 | ||
16 | /** | |
954d51cf | 17 | * Provides access to the SCSS PHP compiler. |
b4cbf821 AE |
18 | * |
19 | * @author Alexander Ebert | |
7b7b9764 | 20 | * @copyright 2001-2019 WoltLab GmbH |
b4cbf821 | 21 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
e71525e4 | 22 | * @package WoltLabSuite\Core\System\Style |
b4cbf821 AE |
23 | */ |
24 | class StyleCompiler extends SingletonFactory { | |
25 | /** | |
954d51cf AE |
26 | * SCSS compiler object |
27 | * @var \Leafo\ScssPhp\Compiler | |
b4cbf821 AE |
28 | */ |
29 | protected $compiler = null; | |
30 | ||
8e40fc29 MS |
31 | /** |
32 | * names of option types which are supported as additional variables | |
7a23a706 | 33 | * @var string[] |
8e40fc29 | 34 | */ |
0f0da0fe | 35 | public static $supportedOptionType = ['boolean', 'float', 'integer', 'radioButton', 'select']; |
8e40fc29 | 36 | |
362af1de AE |
37 | /** |
38 | * file used to store global SCSS declarations, relative to `WCF_DIR` | |
39 | * @var string | |
40 | */ | |
41 | const FILE_GLOBAL_VALUES = 'style/ui/zzz_wsc_style_global_values.scss'; | |
42 | ||
2a340e91 AE |
43 | /** |
44 | * registry keys for data storage | |
45 | * @var string | |
46 | */ | |
47 | const REGISTRY_GLOBAL_VALUES = 'styleGlobalValues'; | |
48 | ||
b4cbf821 | 49 | /** |
0fcfe5f6 | 50 | * @inheritDoc |
b4cbf821 AE |
51 | */ |
52 | protected function init() { | |
954d51cf | 53 | require_once(WCF_DIR.'lib/system/style/scssphp/scss.inc.php'); |
8e87bc50 | 54 | $this->compiler = new Compiler(); |
58a688d5 TD |
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'); | |
954d51cf | 58 | $this->compiler->setImportPaths([WCF_DIR]); |
b4cbf821 AE |
59 | } |
60 | ||
61 | /** | |
954d51cf | 62 | * Compiles SCSS stylesheets. |
b4cbf821 | 63 | * |
4e25add7 | 64 | * @param Style $style |
b4cbf821 AE |
65 | */ |
66 | public function compile(Style $style) { | |
99ee3c6a | 67 | $files = $this->getCoreFiles(); |
b4cbf821 | 68 | |
99ee3c6a | 69 | // read stylesheets in dependency order |
2f873ac2 AE |
70 | $sql = "SELECT filename, application |
71 | FROM wcf".WCF_N."_package_installation_file_log | |
cb75fc92 | 72 | WHERE CONVERT(filename using utf8) REGEXP ? |
99ee3c6a | 73 | AND packageID <> ? |
2f873ac2 | 74 | ORDER BY packageID"; |
b4cbf821 | 75 | $statement = WCF::getDB()->prepareStatement($sql); |
99ee3c6a AE |
76 | $statement->execute([ |
77 | 'style/([a-zA-Z0-9\-\.]+)\.scss', | |
78 | 1 | |
79 | ]); | |
b4cbf821 | 80 | while ($row = $statement->fetchArray()) { |
362af1de AE |
81 | // the global values will always be evaluated last |
82 | if ($row['filename'] === self::FILE_GLOBAL_VALUES) { | |
83 | continue; | |
84 | } | |
85 | ||
6012e566 | 86 | $files[] = Application::getDirectory($row['application']).$row['filename']; |
b4cbf821 AE |
87 | } |
88 | ||
362af1de AE |
89 | // global SCSS |
90 | if (file_exists(WCF_DIR . self::FILE_GLOBAL_VALUES)) { | |
91 | $files[] = WCF_DIR . self::FILE_GLOBAL_VALUES; | |
92 | } | |
93 | ||
4d9f6058 AE |
94 | // get style variables |
95 | $variables = $style->getVariables(); | |
954d51cf AE |
96 | $individualScss = ''; |
97 | if (isset($variables['individualScss'])) { | |
98 | $individualScss = $variables['individualScss']; | |
99 | unset($variables['individualScss']); | |
b4cbf821 AE |
100 | } |
101 | ||
6ff02393 AE |
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)); | |
107 | } | |
108 | $variables['style_image_path'] = "'{$imagePath}'"; | |
109 | ||
d409e4fa | 110 | // apply overrides |
954d51cf AE |
111 | if (isset($variables['overrideScss'])) { |
112 | $lines = explode("\n", StringUtil::unifyNewlines($variables['overrideScss'])); | |
d409e4fa AE |
113 | foreach ($lines as $line) { |
114 | if (preg_match('~^@([a-zA-Z]+): ?([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$~', $line, $matches)) { | |
115 | $variables[$matches[1]] = $matches[2]; | |
116 | } | |
117 | } | |
954d51cf | 118 | unset($variables['overrideScss']); |
d409e4fa AE |
119 | } |
120 | ||
811f5a93 AE |
121 | // api version |
122 | $variables['apiVersion'] = $style->apiVersion; | |
123 | ||
3fe11507 AE |
124 | $parameters = ['scss' => '']; |
125 | EventHandler::getInstance()->fireAction($this, 'compile', $parameters); | |
126 | ||
06a95535 | 127 | $this->compileStylesheet( |
f1c1fc65 | 128 | WCF_DIR.'style/style-'.$style->styleID, |
06a95535 AE |
129 | $files, |
130 | $variables, | |
3fe11507 | 131 | $individualScss . (!empty($parameters['scss']) ? "\n" . $parameters['scss'] : ''), |
a0c6927a | 132 | function($content) use ($style) { |
06a95535 | 133 | return "/* stylesheet for '".$style->styleName."', generated on ".gmdate('r')." -- DO NOT EDIT */\n\n" . $content; |
a0c6927a | 134 | } |
06a95535 AE |
135 | ); |
136 | } | |
137 | ||
138 | /** | |
954d51cf | 139 | * Compiles SCSS stylesheets for ACP usage. |
06a95535 AE |
140 | */ |
141 | public function compileACP() { | |
96ad3d1d MW |
142 | if (substr(WCF_VERSION, 0, 3) == '2.1') { |
143 | // work-around for wcf2.1 update | |
144 | return; | |
145 | } | |
146 | ||
99ee3c6a | 147 | $files = $this->getCoreFiles(); |
f2b50825 | 148 | |
99ee3c6a AE |
149 | // ACP uses a slightly different layout |
150 | $files[] = WCF_DIR . 'acp/style/layout.scss'; | |
b4cbf821 | 151 | |
c7011512 AE |
152 | // include stylesheets from other apps in arbitrary order |
153 | if (PACKAGE_ID) { | |
154 | foreach (ApplicationHandler::getInstance()->getApplications() as $application) { | |
155 | $files = array_merge($files, $this->getAcpStylesheets($application)); | |
156 | } | |
157 | } | |
158 | ||
a0aa4447 AE |
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(); | |
954d51cf | 165 | $variables = []; |
a0aa4447 | 166 | while ($row = $statement->fetchArray()) { |
160bfe44 AE |
167 | $value = $row['defaultValue']; |
168 | if (empty($value)) { | |
169 | $value = '~""'; | |
170 | } | |
171 | ||
172 | $variables[$row['variableName']] = $value; | |
a0aa4447 AE |
173 | } |
174 | ||
b3ac08d1 AE |
175 | $variables['wcfFontFamily'] = $variables['wcfFontFamilyFallback']; |
176 | if (!empty($variables['wcfFontFamilyGoogle'])) { | |
177 | $variables['wcfFontFamily'] = '"' . $variables['wcfFontFamilyGoogle'] . '", ' . $variables['wcfFontFamily']; | |
178 | } | |
179 | ||
99ee3c6a | 180 | $variables['style_image_path'] = "'../images/'"; |
6ff02393 | 181 | |
06a95535 AE |
182 | $this->compileStylesheet( |
183 | WCF_DIR.'acp/style/style', | |
184 | $files, | |
a0aa4447 | 185 | $variables, |
8e87bc50 | 186 | '', |
a0c6927a | 187 | function($content) { |
06a95535 | 188 | // fix relative paths |
556973c1 | 189 | $content = str_replace('../font/', '../../font/', $content); |
06a95535 | 190 | $content = str_replace('../icon/', '../../icon/', $content); |
99ee3c6a | 191 | $content = preg_replace('~\.\./images/~', '../../images/', $content); |
06a95535 AE |
192 | |
193 | return "/* stylesheet for ACP, generated on ".gmdate('r')." -- DO NOT EDIT */\n\n" . $content; | |
a0c6927a | 194 | } |
06a95535 | 195 | ); |
b4cbf821 AE |
196 | } |
197 | ||
99ee3c6a AE |
198 | /** |
199 | * Returns a list of common stylesheets provided by the core. | |
200 | * | |
201 | * @return string[] list of common stylesheets | |
202 | */ | |
203 | protected function getCoreFiles() { | |
204 | $files = []; | |
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)) { | |
208 | continue; | |
209 | } | |
210 | ||
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)) { | |
215 | continue; | |
216 | } | |
217 | ||
218 | $files[] = $file.$innerFile; | |
219 | } | |
220 | closedir($innerHandle); | |
221 | } | |
222 | } | |
223 | ||
224 | closedir($handle); | |
225 | ||
226 | // directory order is not deterministic in some cases | |
227 | sort($files); | |
228 | } | |
229 | ||
230 | return $files; | |
231 | } | |
232 | ||
c7011512 AE |
233 | /** |
234 | * Returns the list of SCSS stylesheets of an application. | |
235 | * | |
236 | * @param Application $application | |
237 | * @return string[] | |
238 | */ | |
239 | protected function getAcpStylesheets(Application $application) { | |
240 | if ($application->packageID == 1) return []; | |
241 | ||
242 | $files = []; | |
243 | ||
244 | $basePath = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $application->getPackage()->packageDir)) . 'acp/style/'; | |
953d6889 AE |
245 | $result = glob($basePath . '*.scss'); |
246 | if (is_array($result)) { | |
247 | foreach ($result as $file) { | |
248 | $files[] = $file; | |
249 | } | |
c7011512 AE |
250 | } |
251 | ||
252 | return $files; | |
253 | } | |
254 | ||
b4cbf821 | 255 | /** |
4d9f6058 | 256 | * Prepares the style compiler by adding variables to environment. |
b4cbf821 | 257 | * |
7a23a706 | 258 | * @param string[] $variables |
b4cbf821 AE |
259 | * @return string |
260 | */ | |
4d9f6058 | 261 | protected function bootstrap(array $variables) { |
b4cbf821 | 262 | // add reset like a boss |
954d51cf | 263 | $content = $this->prepareFile(WCF_DIR.'style/bootstrap/reset.scss'); |
b4cbf821 | 264 | |
b4cbf821 AE |
265 | // apply style variables |
266 | $this->compiler->setVariables($variables); | |
267 | ||
268 | // add mixins | |
954d51cf | 269 | $content .= $this->prepareFile(WCF_DIR.'style/bootstrap/mixin.scss'); |
b4cbf821 | 270 | |
e71525e4 | 271 | // add newer mixins added with version 3.0 |
5908f54f AE |
272 | foreach (glob(WCF_DIR.'style/bootstrap/mixin/*.scss') as $mixin) { |
273 | $content .= $this->prepareFile($mixin); | |
274 | } | |
275 | ||
b4cbf821 AE |
276 | return $content; |
277 | } | |
278 | ||
279 | /** | |
954d51cf | 280 | * Prepares a SCSS stylesheet for importing. |
b4cbf821 AE |
281 | * |
282 | * @param string $filename | |
283 | * @return string | |
2b770bdd | 284 | * @throws SystemException |
b4cbf821 AE |
285 | */ |
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"); | |
289 | } | |
290 | ||
291 | // use a relative path | |
292 | $filename = FileUtil::getRelativePath(WCF_DIR, dirname($filename)) . basename($filename); | |
293 | return '@import "'.$filename.'";'."\n"; | |
294 | } | |
06a95535 AE |
295 | |
296 | /** | |
954d51cf | 297 | * Compiles SCSS stylesheets into one CSS-stylesheet and writes them |
06a95535 AE |
298 | * to filesystem. Please be aware not to append '.css' within $filename! |
299 | * | |
ac52543a MS |
300 | * @param string $filename |
301 | * @param string[] $files | |
302 | * @param string[] $variables | |
303 | * @param string $individualScss | |
a0c6927a | 304 | * @param callable $callback |
2b770bdd | 305 | * @throws SystemException |
06a95535 | 306 | */ |
a0c6927a | 307 | protected function compileStylesheet($filename, array $files, array $variables, $individualScss, callable $callback) { |
2c47fd2c MS |
308 | foreach ($variables as &$value) { |
309 | if (StringUtil::startsWith($value, '../')) { | |
310 | $value = '~"'.$value.'"'; | |
311 | } | |
312 | } | |
313 | unset($value); | |
314 | ||
1223c43c AE |
315 | $variables['wcfFontFamily'] = $variables['wcfFontFamilyFallback']; |
316 | if (!empty($variables['wcfFontFamilyGoogle'])) { | |
852fefda AE |
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'] . '"'; | |
320 | ||
321 | $variables['wcfFontFamily'] = $variables['wcfFontFamilyGoogle'] . ', ' . $variables['wcfFontFamily']; | |
1223c43c AE |
322 | } |
323 | ||
954d51cf | 324 | // add options as SCSS variables |
b8d20540 MS |
325 | if (PACKAGE_ID) { |
326 | foreach (Option::getOptions() as $constantName => $option) { | |
327 | if (in_array($option->optionType, static::$supportedOptionType)) { | |
63b9817b | 328 | $variables['wcf_option_'.mb_strtolower($constantName)] = is_int($option->optionValue) ? $option->optionValue : '"'.$option->optionValue.'"'; |
b8d20540 MS |
329 | } |
330 | } | |
811f5a93 AE |
331 | |
332 | // api version | |
333 | if (!isset($variables['apiVersion'])) $variables['apiVersion'] = Style::API_VERSION; | |
b8d20540 MS |
334 | } |
335 | else { | |
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"'; | |
811f5a93 AE |
340 | |
341 | $variables['apiVersion'] = Style::API_VERSION; | |
b8d20540 MS |
342 | } |
343 | ||
811f5a93 AE |
344 | // convert into numeric value for comparison, e.g. `3.1` -> `31` |
345 | $variables['apiVersion'] = str_replace('.', '', $variables['apiVersion']); | |
346 | ||
954d51cf AE |
347 | // build SCSS bootstrap |
348 | $scss = $this->bootstrap($variables); | |
06a95535 | 349 | foreach ($files as $file) { |
954d51cf | 350 | $scss .= $this->prepareFile($file); |
06a95535 AE |
351 | } |
352 | ||
954d51cf AE |
353 | // append individual CSS/SCSS |
354 | if ($individualScss) { | |
355 | $scss .= $individualScss; | |
06a95535 AE |
356 | } |
357 | ||
358 | try { | |
954d51cf AE |
359 | $this->compiler->setFormatter('Leafo\ScssPhp\Formatter\Crunched'); |
360 | $content = $this->compiler->compile($scss); | |
06a95535 AE |
361 | } |
362 | catch (\Exception $e) { | |
954d51cf | 363 | throw new SystemException("Could not compile SCSS: ".$e->getMessage(), 0, '', $e); |
06a95535 AE |
364 | } |
365 | ||
366 | $content = $callback($content); | |
367 | ||
368 | // write stylesheet | |
369 | file_put_contents($filename.'.css', $content); | |
18c6f9fd | 370 | FileUtil::makeWritable($filename.'.css'); |
06a95535 AE |
371 | |
372 | // convert stylesheet to RTL | |
c035ecce | 373 | $content = StyleUtil::convertCSSToRTL($content); |
06a95535 | 374 | |
553b425e AE |
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; }'; | |
378 | ||
06a95535 AE |
379 | // write stylesheet for RTL |
380 | file_put_contents($filename.'-rtl.css', $content); | |
18c6f9fd | 381 | FileUtil::makeWritable($filename.'-rtl.css'); |
06a95535 | 382 | } |
b4cbf821 | 383 | } |