From 63c3095529c9babfacf414c9f593ff560e5a6cc2 Mon Sep 17 00:00:00 2001 From: James <78121730+itsnewtjam@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:53:48 -0400 Subject: [PATCH 1/2] improve include, extend support for dot notation in expressions (#25) --- src/Quark/QuarkCompiler.php | 49 +++++++++++++++++++++----- src/Quark/QuarkEngine.php | 33 +++++++++++++++++ tests/Mocks/Quark/NestedObject.php | 12 +++++++ tests/Mocks/Quark/SimpleObject.php | 21 +++++++++++ tests/Unit/Quark/QuarkCompilerTest.php | 2 +- tests/Unit/Quark/QuarkEngineTest.php | 46 ++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 tests/Mocks/Quark/NestedObject.php create mode 100644 tests/Mocks/Quark/SimpleObject.php diff --git a/src/Quark/QuarkCompiler.php b/src/Quark/QuarkCompiler.php index 2158538..39fb6dc 100644 --- a/src/Quark/QuarkCompiler.php +++ b/src/Quark/QuarkCompiler.php @@ -77,8 +77,30 @@ private function registerBuiltinDirectives(): void { }; $this->directives['include'] = function ($args) { - $template = trim($args, '"\''); - return "\$__quark->skipRootLayout();\necho \$__quark->render('{$template}');\n"; + if (preg_match('/^["\']?([^"\'\s]+)["\']?(?:\s*\[(.+)\])?$/', $args, $matches)) { + $template = trim($matches[1], '"\''); + $data = '[]'; + if (count($matches) > 2) { + $variables = array_map('trim', explode(',', $matches[2])); + + $cleanVars = []; + foreach ($variables as $var) { + $var = ltrim($var, '$'); + + if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $var)) { + $cleanVars[] = $var; + } + } + + $quotedVars = array_map(function($var) { + return "'" . $var . "'"; + }, $cleanVars); + + $data = "compact(" . implode(', ', $quotedVars) . ")"; + } + return "echo \$__quark->render('{$template}', {$data});\n"; + } + throw new \Exception("Invalid include syntax: {$args}"); }; $this->directives['if'] = fn($args) => "if ({$args}) {\n"; @@ -215,16 +237,27 @@ private function normalizeVariable(string $expression): string { return $expression; } - if (strpos($expression, '.') !== false) { - if (preg_match('/^(\w+)\.(.+)$/', $expression, $matches)) { - return '$' . $matches[1] . '->' . str_replace('.', '->', $matches[2]); + if (strpos($expression, '->') !== false) { + if (preg_match('/^(\w+)->(.+)$/', $expression, $matches)) { + return '$' . $matches[1] . '->' . $matches[2]; } } - if (strpos($expression, '->') !== false) { - if (preg_match('/^(\w+)->(.+)$/', $expression, $matches)) { - return '$' . $matches[1] . '->' . str_replace('.', '->', $matches[2]); + if (strpos($expression, '.') !== false) { + $parts = explode('.', $expression); + $variable = '$' . array_shift($parts); + foreach ($parts as $part) { + if (preg_match('/^(\w+)\((.*)\)$/', $part, $matches)) { + $method = $matches[1]; + $args = $matches[2]; + $variable .= '->' . $method . '(' . $args . ')'; + } elseif (is_numeric($part)) { + $variable .= '[' . $part . ']'; + } else { + $variable = "\$__quark->access({$variable}, '{$part}')"; + } } + return $variable; } if (preg_match('/^\w+$/', $expression)) { diff --git a/src/Quark/QuarkEngine.php b/src/Quark/QuarkEngine.php index d9c448a..a4f60a9 100644 --- a/src/Quark/QuarkEngine.php +++ b/src/Quark/QuarkEngine.php @@ -207,6 +207,39 @@ public function escape(mixed $value, string $context = 'html'): string { } } + /** + * Smart accessor for arrays, objects, and ArrayAccess + * + * @param mixed $value The value to access on + * @param string $key The member to access + */ + public function access(mixed $value, string $key): mixed { + if ($value === null) { + return null; + } + + if (is_array($value) || $value instanceof \ArrayAccess) { + return $value[$key] ?? null; + } + + if (is_object($value)) { + if (property_exists($value, $key)) { + return $value->$key; + } + + $getter = 'get' . ucfirst($key); + if (method_exists($value, $getter)) { + return $value->$getter(); + } + + if (method_exists($value, '__get')) { + return $value->__get($key); + } + } + + return null; + } + /** * Apply a filter to a value * diff --git a/tests/Mocks/Quark/NestedObject.php b/tests/Mocks/Quark/NestedObject.php new file mode 100644 index 0000000..6fe7f0a --- /dev/null +++ b/tests/Mocks/Quark/NestedObject.php @@ -0,0 +1,12 @@ +nestedObject = new NestedObject(); + } + + public function method(): string { + return 'Test Method'; + } + + public function getNested(): NestedObject { + return $this->nestedObject; + } +} diff --git a/tests/Unit/Quark/QuarkCompilerTest.php b/tests/Unit/Quark/QuarkCompilerTest.php index 918d3e4..c3ac740 100644 --- a/tests/Unit/Quark/QuarkCompilerTest.php +++ b/tests/Unit/Quark/QuarkCompilerTest.php @@ -151,7 +151,7 @@ public function testNormalizeVariableDotToProperty(): void { $method->setAccessible(true); $this->assertEquals( - '$test->property', + '$__quark->access($test, \'property\')', $method->invoke($this->compiler, 'test.property') ); } diff --git a/tests/Unit/Quark/QuarkEngineTest.php b/tests/Unit/Quark/QuarkEngineTest.php index 8859b28..64a496a 100644 --- a/tests/Unit/Quark/QuarkEngineTest.php +++ b/tests/Unit/Quark/QuarkEngineTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\Quark; +use Tests\Mocks\Quark\SimpleObject; use Tests\QuarkTestCase; class QuarkEngineTest extends QuarkTestCase { @@ -214,6 +215,51 @@ public function testEscapeRaw(): void { ); } + public function testDotWithArray(): void { + $this->createTestTemplate('test', '
{{ test.title }}
'); + + $this->assertEquals( + '
Test Value
', + $this->engine->render('test', ['test' => ['title' => 'Test Value']]) + ); + } + + public function testDotWithObject(): void { + $this->createTestTemplate('test', '
{{ test.property }}
'); + + $this->assertEquals( + '
Test Property
', + $this->engine->render('test', ['test' => new SimpleObject()]) + ); + } + + public function testDotWithMethod(): void { + $this->createTestTemplate('test', '
{{ test.method() }}
'); + + $this->assertEquals( + '
Test Method
', + $this->engine->render('test', ['test' => new SimpleObject()]) + ); + } + + public function testDotWithMixed(): void { + $this->createTestTemplate('test', '
{{ test.nested.method() }}
'); + + $this->assertEquals( + '
Nested Method
', + $this->engine->render('test', ['test' => new SimpleObject()]) + ); + } + + public function testDotWithNestedMethods(): void { + $this->createTestTemplate('test', '
{{ test.getNested().method() }}
'); + + $this->assertEquals( + '
Nested Method
', + $this->engine->render('test', ['test' => new SimpleObject()]) + ); + } + public function testFilterUpper(): void { $this->assertEquals( 'TEST', From 2a71f31ac81cf60ab6e8afd3e9e57ff05b850bfa Mon Sep 17 00:00:00 2001 From: James <78121730+itsnewtjam@users.noreply.github.com> Date: Tue, 30 Sep 2025 09:55:35 -0400 Subject: [PATCH 2/2] bump version (#26) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 76594e4..b74a529 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "newtron/core", - "version": "0.1.2", + "version": "0.1.3", "type": "library", "description": "Core framework package for Newtron", "homepage": "https://github.com/newtron-framework/core",