Skip to content

Commit e2f9412

Browse files
committed
Risma 0.2.0
1 parent c11127b commit e2f9412

2 files changed

Lines changed: 50 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Includes the following helpers:
1616
- [Mem](https://github.com/nabeghe/mem-php) <small>v1.2.0</small>
1717
- [ProcessFinger](https://github.com/nabeghe/process-finger-php) <small>v0.1.2</small>
1818
- [Reflecty](https://github.com/nabeghe/reflecty-php) <small>v0.5.2</small>
19-
- [Risma](https://github.com/nabeghe/risma-php) <small>v0.1.0</small>
19+
- [Risma](https://github.com/nabeghe/risma-php) <small>v0.2.0</small>
2020
- [Servery](https://github.com/nabeghe/servery-php) <small>v0.3.0</small>
2121
- [Shortnum](https://github.com/nabeghe/shortnum-php) <small>v1.0.0</small>
2222
- [SimpleCipher](https://github.com/nabeghe/simple-cipher-php) <small>v1.0.0</small>

src/Risma/Risma.php

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
/**
77
* Risma
88
* A lightweight and flexible string processing engine for PHP.
9-
* eveloped by https://github.com/nabeghe.
9+
* developed by [https://github.com/nabeghe](https://github.com/nabeghe).
1010
*/
1111
class Risma
1212
{
@@ -20,6 +20,16 @@ class Risma
2020
*/
2121
protected array $classes = [];
2222

23+
/**
24+
* @var int Maximum recursion depth to prevent infinite loops.
25+
*/
26+
protected int $maxDepth = 10;
27+
28+
/**
29+
* @var int Current recursion depth tracker.
30+
*/
31+
protected int $currentDepth = 0;
32+
2333
public function __construct()
2434
{
2535
$this->defineDefaultFuncs();
@@ -37,11 +47,19 @@ protected function defineDefaultFuncs(): void
3747
$this->funcs['ok'] ??= function ($value) {
3848
return $value ? '1' : '0';
3949
};
50+
51+
$this->funcs['prepend'] ??= function ($input, $prefix) {
52+
return $prefix.$input;
53+
};
54+
55+
$this->funcs['append'] ??= function ($input, $suffix) {
56+
return $input.$suffix;
57+
};
4058
}
4159

4260
/**
4361
* Register a custom function.
44-
* * @param string $name The name used in the template.
62+
* @param string $name The name used in the template.
4563
* @param callable $callback The logic to execute.
4664
*/
4765
public function addFunc(string $name, callable $callback): void
@@ -51,7 +69,7 @@ public function addFunc(string $name, callable $callback): void
5169

5270
/**
5371
* Register a class to expose its methods to the engine.
54-
* * @param string $className Full namespace of the class.
72+
* @param string $className Full namespace of the class.
5573
*/
5674
public function addClass(string $className): void
5775
{
@@ -62,15 +80,21 @@ public function addClass(string $className): void
6280

6381
/**
6482
* Renders the template string by replacing placeholders.
65-
* * @param string $text The raw string with placeholders like {var.func}.
83+
* Uses recursive regex to match nested braces correctly.
84+
* @param string $text The raw string with placeholders like {var.func}.
6685
* @param array $vars Key-value pairs of data.
6786
* @param bool $default If true, returns empty string for missing variables.
6887
* @return string The processed text.
6988
* @throws Exception
7089
*/
7190
public function render(string $text, array $vars, bool $default = true): string
7291
{
73-
$pattern = '/(!?)\{\s*(.*?)\s*\}/s';
92+
// Reset depth counter at the start of render
93+
$this->currentDepth = 0;
94+
95+
// Use recursive regex pattern to match balanced curly braces
96+
// (?1) refers to the first capturing group recursively
97+
$pattern = '/(!?)\{\s*((?:[^{}]|(?R))*)\s*\}/';
7498

7599
return preg_replace_callback($pattern, function ($matches) use ($vars, $default) {
76100
$isEscaped = !empty($matches[1]);
@@ -80,9 +104,25 @@ public function render(string $text, array $vars, bool $default = true): string
80104
return '{'.$content.'}';
81105
}
82106

107+
// Check recursion depth
108+
$this->currentDepth++;
109+
if ($this->currentDepth > $this->maxDepth) {
110+
$this->currentDepth--;
111+
throw new Exception("Maximum recursion depth exceeded.");
112+
}
113+
83114
try {
84-
return $this->processExpression($content, $vars, $default);
115+
// First recursively render any nested placeholders
116+
$renderedContent = $this->render($content, $vars, $default);
117+
118+
// Then process the expression itself
119+
$result = $this->processExpression($renderedContent, $vars, $default);
120+
121+
$this->currentDepth--;
122+
return $result;
85123
} catch (Throwable $e) {
124+
$this->currentDepth--;
125+
86126
// Modified logic: If default is false, re-throw the exception for PHPUnit
87127
if (!$default) {
88128
throw $e;
@@ -94,7 +134,7 @@ public function render(string $text, array $vars, bool $default = true): string
94134

95135
/**
96136
* Processes the content inside a single {} block.
97-
* * @throws Exception
137+
* @throws Exception
98138
*/
99139
protected function processExpression(string $expression, array $vars, bool $default): string
100140
{
@@ -135,7 +175,7 @@ protected function processExpression(string $expression, array $vars, bool $defa
135175

136176
/**
137177
* Executes a single function within the chain.
138-
* * @param string $token The function part, e.g., "func1" or "func('arg')".
178+
* @param string $token The function part, e.g., "func1" or "func('arg')".
139179
* @param mixed $prevValue The value from the previous step in the chain.
140180
* @param bool $isFirstCall Whether this is the start of a direct @ call.
141181
* @throws Exception
@@ -179,7 +219,7 @@ protected function executeFunction(string $token, $prevValue, bool $isFirstCall)
179219

180220
/**
181221
* Resolves the function name to a callable (Custom -> Class -> Global).
182-
* * @throws Exception
222+
* @throws Exception
183223
*/
184224
protected function resolveCallback(string $name): callable
185225
{

0 commit comments

Comments
 (0)