diff --git a/framework/core/src/Extend/Theme.php b/framework/core/src/Extend/Theme.php index 74fc4fa0e..26a860c4e 100644 --- a/framework/core/src/Extend/Theme.php +++ b/framework/core/src/Extend/Theme.php @@ -12,11 +12,13 @@ namespace Flarum\Extend; use Flarum\Extension\Extension; use Flarum\Frontend\Assets; use Illuminate\Contracts\Container\Container; +use RuntimeException; class Theme implements ExtenderInterface { private $lessImportOverrides = []; private $fileSourceOverrides = []; + private $customFunctions = []; /** * This can be used to override LESS files that are imported within the code. @@ -52,8 +54,58 @@ class Theme implements ExtenderInterface return $this; } + /** + * This method allows you to add custom Less functions. + * + * All custom Less functions may only return numbers, strings or booleans. + * + * **Example usage:** + * ```php + * (new Extend\Theme) + * ->addCustomLessFunction('is-flarum', function (mixed $text) { + * return strtolower($text) === 'flarum' + * }), + * ``` + * + * @see https://leafo.net/lessphp/docs/#custom_functions + * + * @param string $functionName Name of the function identifier. + * @param callable $callable The PHP function to run when the Less function is called. + * @return self + */ + public function addCustomLessFunction(string $functionName, callable $callable): self + { + $this->customFunctions[$functionName] = function (...$args) use ($callable, $functionName) { + $argVals = array_map(function ($arg) { + return $arg->value; + }, $args); + + $return = $callable(...$argVals); + + if (is_bool($return)) { + return new \Less_Tree_Quoted('', $return ? 'true' : 'false'); + } + + if (is_string($return)) { + return new \Less_Tree_Quoted('', $return); + } + + if (is_numeric($return)) { + return new \Less_Tree_Dimension($return); + } + + throw new RuntimeException('Custom Less function `'.$functionName.'` must only return a string, number or boolean.'); + }; + + return $this; + } + public function extend(Container $container, Extension $extension = null) { + $container->extend('flarum.frontend.custom_less_functions', function (array $customFunctions) { + return array_merge($customFunctions, $this->customFunctions); + }); + $container->extend('flarum.assets.factory', function (callable $factory) { return function (...$args) use ($factory) { /** @var Assets $assets */ diff --git a/framework/core/src/Frontend/Assets.php b/framework/core/src/Frontend/Assets.php index d52ecdfa4..95d91f2ea 100644 --- a/framework/core/src/Frontend/Assets.php +++ b/framework/core/src/Frontend/Assets.php @@ -62,12 +62,18 @@ class Assets */ protected $fileSourceOverrides = []; - public function __construct(string $name, Filesystem $assetsDir, string $cacheDir = null, array $lessImportDirs = null) + /** + * @var array + */ + protected $customFunctions = []; + + public function __construct(string $name, Filesystem $assetsDir, string $cacheDir = null, array $lessImportDirs = null, array $customFunctions = []) { $this->name = $name; $this->assetsDir = $assetsDir; $this->cacheDir = $cacheDir; $this->lessImportDirs = $lessImportDirs; + $this->customFunctions = $customFunctions; } public function js($sources) @@ -173,6 +179,8 @@ class Assets $compiler->setFileSourceOverrides($this->fileSourceOverrides); } + $compiler->setCustomFunctions($this->customFunctions); + return $compiler; } diff --git a/framework/core/src/Frontend/Compiler/LessCompiler.php b/framework/core/src/Frontend/Compiler/LessCompiler.php index a88061624..e6c92c928 100644 --- a/framework/core/src/Frontend/Compiler/LessCompiler.php +++ b/framework/core/src/Frontend/Compiler/LessCompiler.php @@ -29,6 +29,11 @@ class LessCompiler extends RevisionCompiler */ protected $importDirs = []; + /** + * @var array + */ + protected $customFunctions = []; + /** * @var Collection */ @@ -69,6 +74,11 @@ class LessCompiler extends RevisionCompiler $this->fileSourceOverrides = new Collection($fileSourceOverrides); } + public function setCustomFunctions(array $customFunctions) + { + $this->customFunctions = $customFunctions; + } + /** * @throws \Less_Exception_Parser */ @@ -99,6 +109,10 @@ class LessCompiler extends RevisionCompiler } } + foreach ($this->customFunctions as $name => $callback) { + $parser->registerFunction($name, $callback); + } + return $parser->getCss(); } diff --git a/framework/core/src/Frontend/FrontendServiceProvider.php b/framework/core/src/Frontend/FrontendServiceProvider.php index fafa78e79..86af8f1bb 100644 --- a/framework/core/src/Frontend/FrontendServiceProvider.php +++ b/framework/core/src/Frontend/FrontendServiceProvider.php @@ -31,7 +31,9 @@ class FrontendServiceProvider extends AbstractServiceProvider $assets = new Assets( $name, $container->make('filesystem')->disk('flarum-assets'), - $paths->storage + $paths->storage, + null, + $container->make('flarum.frontend.custom_less_functions') ); $assets->setLessImportDirs([ @@ -110,6 +112,22 @@ class FrontendServiceProvider extends AbstractServiceProvider } ); + $this->container->singleton( + 'flarum.frontend.custom_less_functions', + function (Container $container) { + $extensionsEnabled = json_decode($container->make(SettingsRepositoryInterface::class)->get('extensions_enabled')); + + // Please note that these functions do not go through the same transformation which the Theme extender's + // `addCustomLessFunction` method does. You'll need to use the correct Less tree return type, and get + // parameter values with `$arg->value`. + return [ + 'is-extension-enabled' => function (\Less_Tree_Quoted $extensionId) use ($extensionsEnabled) { + return new \Less_Tree_Quoted('', in_array($extensionId->value, $extensionsEnabled) ? 'true' : 'false'); + } + ]; + } + ); + $this->container->singleton(TitleDriverInterface::class, function (Container $container) { return $container->make(BasicTitleDriver::class); }); diff --git a/framework/core/tests/fixtures/less/custom_function.less b/framework/core/tests/fixtures/less/custom_function.less new file mode 100644 index 000000000..63d5ea7a1 --- /dev/null +++ b/framework/core/tests/fixtures/less/custom_function.less @@ -0,0 +1,7 @@ +.dummy_func_test when (is-flarum("flarum") = true) { + color: green; +} +.dummy_func_test2 { + --x: is-flarum("not flarum") * 10; + --y: is-gt(1, 2); +} diff --git a/framework/core/tests/integration/extenders/ThemeTest.php b/framework/core/tests/integration/extenders/ThemeTest.php index 87b828494..ba8e50e0b 100644 --- a/framework/core/tests/integration/extenders/ThemeTest.php +++ b/framework/core/tests/integration/extenders/ThemeTest.php @@ -104,4 +104,32 @@ class ThemeTest extends TestCase $this->assertStringContainsString('Less_Exception_Compiler', $response->getBody()->getContents()); $this->assertEquals(500, $response->getStatusCode()); } + + /** + * @test + */ + public function theme_extender_can_add_custom_function() + { + $this->extend( + (new Extend\Frontend('forum')) + ->css(__DIR__.'/../../fixtures/less/custom_function.less'), + (new Extend\Theme) + ->addCustomLessFunction('is-flarum', function ($text) { + return strtolower($text) === 'flarum' ? 'true' : 100; + }) + ->addCustomLessFunction('is-gt', function ($a, $b) { + return $a > $b; + }) + ); + + $response = $this->send($this->request('GET', '/')); + + $this->assertEquals(200, $response->getStatusCode()); + + $cssFilePath = $this->app()->getContainer()->make('filesystem')->disk('flarum-assets')->path('forum.css'); + $contents = file_get_contents($cssFilePath); + + $this->assertStringContainsString('.dummy_func_test{color:green}', $contents); + $this->assertStringContainsString('.dummy_func_test2{--x:1000;--y:false}', $contents); + } }