From d05889fa8eec0bdb211532bc7066f73cd066c36f Mon Sep 17 00:00:00 2001 From: Mathieu Ducharme Date: Thu, 3 Sep 2026 19:41:31 -0400 Subject: [PATCH] fix(admin): support Mustache 3.2 dotted-name lookup in AssetsHelpers Mustache 3.2 invokes callables while resolving names like assets.output.css and then interpolates the helper object. Return $this on zero-arg invoke and run the pending action from __toString(). --- .../Charcoal/Admin/Mustache/AssetsHelpers.php | 30 ++++++++- .../Admin/Mustache/AssetsHelpersTest.php | 65 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 packages/admin/tests/Charcoal/Admin/Mustache/AssetsHelpersTest.php diff --git a/packages/admin/src/Charcoal/Admin/Mustache/AssetsHelpers.php b/packages/admin/src/Charcoal/Admin/Mustache/AssetsHelpers.php index 5e2658219..986e9ac48 100644 --- a/packages/admin/src/Charcoal/Admin/Mustache/AssetsHelpers.php +++ b/packages/admin/src/Charcoal/Admin/Mustache/AssetsHelpers.php @@ -76,12 +76,23 @@ protected function reset() /** * Magic: Render the Mustache section. * + * Mustache 3+ may invoke callables while resolving dotted names + * (e.g. `assets.output.css`) with zero arguments. In that case, + * return `$this` so `__get()` can continue. + * + * Mustache 3.2 interpolates the final dotted-name value instead of + * invoking it as a lambda. `__toString()` runs the pending action. + * * @param string $text The translation key. * @param LambdaHelper|null $helper For rendering strings in the current context. - * @return string + * @return string|self */ public function __invoke($text = null, LambdaHelper $helper = null) { + if (func_num_args() === 0 || $this->action === null) { + return $this; + } + if ($helper) { $text = (string)$helper->render($text); } @@ -97,6 +108,23 @@ public function __invoke($text = null, LambdaHelper $helper = null) return $text; } + /** + * Magic: Render the pending action when Mustache interpolates this helper. + * + * @return string + */ + public function __toString(): string + { + if ($this->action === null) { + return ''; + } + + $return = $this->{$this->action}($this->collection, ''); + $this->reset(); + + return (string)$return; + } + /** * Magic: Determine if a property is set and is not NULL. * diff --git a/packages/admin/tests/Charcoal/Admin/Mustache/AssetsHelpersTest.php b/packages/admin/tests/Charcoal/Admin/Mustache/AssetsHelpersTest.php new file mode 100644 index 000000000..04b85d6c6 --- /dev/null +++ b/packages/admin/tests/Charcoal/Admin/Mustache/AssetsHelpersTest.php @@ -0,0 +1,65 @@ +assets = new AssetManager(); + $this->assets->set('css', new AssetCollection([ + new StringAsset('.login { color: red; }'), + ])); + + $this->obj = new AssetsHelpers([ + 'assets' => $this->assets, + ]); + } + + /** + * @return void + */ + public function testDottedOutputWithMustache() + { + $mustache = new MustacheEngine([ + 'helpers' => $this->obj->toArray(), + 'strict_callables' => true, + ]); + + $this->assertEquals( + '{{=<<<<% %>>>>=}}.login { color: red; }<<<<%={{ }}=%>>>>', + $mustache->render('{{& assets.output.css }}') + ); + } + + /** + * @return void + */ + public function testInvokeWithoutArgumentsReturnsSelf() + { + $this->assertSame($this->obj, ($this->obj)()); + } +}