Skip to content

Commit c11127b

Browse files
committed
Add Risma 0.1.0
1 parent 78697a2 commit c11127b

3 files changed

Lines changed: 258 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +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>
1920
- [Servery](https://github.com/nabeghe/servery-php) <small>v0.3.0</small>
2021
- [Shortnum](https://github.com/nabeghe/shortnum-php) <small>v1.0.0</small>
2122
- [SimpleCipher](https://github.com/nabeghe/simple-cipher-php) <small>v1.0.0</small>

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"Nabeghe\\Mem\\": "src/Mem",
3434
"Nabeghe\\ProcessFinger\\": "src/ProcessFinger",
3535
"Nabeghe\\Reflecty\\": "src/Reflecty",
36+
"Nabeghe\\Risma\\": "src/Risma",
3637
"Nabeghe\\Servery\\": "src/Servery",
3738
"Nabeghe\\Shortnum\\": "src/Shortnum",
3839
"Nabeghe\\SimpleCipher\\": "src/SimpleCipher",

src/Risma/Risma.php

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
<?php namespace Nabeghe\Risma;
2+
3+
use Exception;
4+
use Throwable;
5+
6+
/**
7+
* Risma
8+
* A lightweight and flexible string processing engine for PHP.
9+
* eveloped by https://github.com/nabeghe.
10+
*/
11+
class Risma
12+
{
13+
/**
14+
* @var array Custom functions registry.
15+
*/
16+
protected array $funcs = [];
17+
18+
/**
19+
* @var array Registered classes for method resolution.
20+
*/
21+
protected array $classes = [];
22+
23+
public function __construct()
24+
{
25+
$this->defineDefaultFuncs();
26+
}
27+
28+
/**
29+
* Defines built-in default functions.
30+
*/
31+
protected function defineDefaultFuncs(): void
32+
{
33+
$this->funcs['exists'] ??= function ($value) {
34+
return ($value === null || $value === '') ? '0' : '1';
35+
};
36+
37+
$this->funcs['ok'] ??= function ($value) {
38+
return $value ? '1' : '0';
39+
};
40+
}
41+
42+
/**
43+
* Register a custom function.
44+
* * @param string $name The name used in the template.
45+
* @param callable $callback The logic to execute.
46+
*/
47+
public function addFunc(string $name, callable $callback): void
48+
{
49+
$this->funcs[$name] = $callback;
50+
}
51+
52+
/**
53+
* Register a class to expose its methods to the engine.
54+
* * @param string $className Full namespace of the class.
55+
*/
56+
public function addClass(string $className): void
57+
{
58+
if (class_exists($className)) {
59+
$this->classes[] = $className;
60+
}
61+
}
62+
63+
/**
64+
* Renders the template string by replacing placeholders.
65+
* * @param string $text The raw string with placeholders like {var.func}.
66+
* @param array $vars Key-value pairs of data.
67+
* @param bool $default If true, returns empty string for missing variables.
68+
* @return string The processed text.
69+
* @throws Exception
70+
*/
71+
public function render(string $text, array $vars, bool $default = true): string
72+
{
73+
$pattern = '/(!?)\{\s*(.*?)\s*\}/s';
74+
75+
return preg_replace_callback($pattern, function ($matches) use ($vars, $default) {
76+
$isEscaped = !empty($matches[1]);
77+
$content = $matches[2];
78+
79+
if ($isEscaped) {
80+
return '{'.$content.'}';
81+
}
82+
83+
try {
84+
return $this->processExpression($content, $vars, $default);
85+
} catch (Throwable $e) {
86+
// Modified logic: If default is false, re-throw the exception for PHPUnit
87+
if (!$default) {
88+
throw $e;
89+
}
90+
return '';
91+
}
92+
}, $text);
93+
}
94+
95+
/**
96+
* Processes the content inside a single {} block.
97+
* * @throws Exception
98+
*/
99+
protected function processExpression(string $expression, array $vars, bool $default): string
100+
{
101+
// Split the chain while respecting parentheses and quotes
102+
$chain = $this->splitChain($expression);
103+
104+
if (empty($chain)) {
105+
return '';
106+
}
107+
108+
$head = array_shift($chain);
109+
$value = null;
110+
111+
// Check for direct function call (starts with @)
112+
if (substr($head, 0, 1) === '@') {
113+
$funcName = substr($head, 1);
114+
$value = $this->executeFunction($funcName, null, true);
115+
} else {
116+
// Handle variable replacement
117+
if (array_key_exists($head, $vars)) {
118+
$value = $vars[$head];
119+
} else {
120+
if ($default) {
121+
$value = '';
122+
} else {
123+
throw new Exception("Variable '$head' is undefined.");
124+
}
125+
}
126+
}
127+
128+
// Process the rest of the function chain
129+
foreach ($chain as $item) {
130+
$value = $this->executeFunction($item, $value, false);
131+
}
132+
133+
return (string) $value;
134+
}
135+
136+
/**
137+
* Executes a single function within the chain.
138+
* * @param string $token The function part, e.g., "func1" or "func('arg')".
139+
* @param mixed $prevValue The value from the previous step in the chain.
140+
* @param bool $isFirstCall Whether this is the start of a direct @ call.
141+
* @throws Exception
142+
*/
143+
protected function executeFunction(string $token, $prevValue, bool $isFirstCall)
144+
{
145+
if (preg_match('/^([a-zA-Z0-9_\\\:]+)(?:\((.*)\))?$/s', $token, $matches)) {
146+
$funcName = $matches[1];
147+
$argsString = $matches[2] ?? null;
148+
149+
$callable = $this->resolveCallback($funcName);
150+
$args = [];
151+
152+
if ($argsString !== null && trim($argsString) !== '') {
153+
try {
154+
$code = "return [$argsString];";
155+
$args = eval($code);
156+
} catch (Throwable $e) {
157+
throw new Exception("Error parsing arguments for '$funcName'.");
158+
}
159+
}
160+
161+
if (!$isFirstCall) {
162+
// Check if user specified where the piped value should go using '$'
163+
$placeholderIndex = array_search('$', $args, true);
164+
165+
if ($placeholderIndex !== false) {
166+
// Replace '$' with the actual previous value
167+
$args[$placeholderIndex] = $prevValue;
168+
} else {
169+
// Default behavior: prepend to arguments
170+
array_unshift($args, $prevValue);
171+
}
172+
}
173+
174+
return call_user_func_array($callable, $args);
175+
}
176+
177+
throw new Exception("Invalid syntax: $token");
178+
}
179+
180+
/**
181+
* Resolves the function name to a callable (Custom -> Class -> Global).
182+
* * @throws Exception
183+
*/
184+
protected function resolveCallback(string $name): callable
185+
{
186+
// 1. Check custom registered functions
187+
if (isset($this->funcs[$name])) {
188+
return $this->funcs[$name];
189+
}
190+
191+
// 2. Check registered class methods
192+
foreach ($this->classes as $class) {
193+
if (method_exists($class, $name)) {
194+
return [$class, $name];
195+
}
196+
}
197+
198+
// 3. Check global PHP functions
199+
if (function_exists($name)) {
200+
return $name;
201+
}
202+
203+
throw new Exception("Function or method '$name' not found.");
204+
}
205+
206+
/**
207+
* Splits the chain by dot while ignoring dots inside quotes or parentheses.
208+
*/
209+
protected function splitChain(string $str): array
210+
{
211+
$parts = [];
212+
$buffer = '';
213+
$stack = 0; // Parentheses stack
214+
$inQuote = false;
215+
$quoteChar = '';
216+
217+
$len = strlen($str);
218+
for ($i = 0; $i < $len; $i++) {
219+
$char = $str[$i];
220+
221+
// Quote management
222+
if (($char === '"' || $char === "'") && ($i === 0 || $str[$i - 1] !== '\\')) {
223+
if (!$inQuote) {
224+
$inQuote = true;
225+
$quoteChar = $char;
226+
} elseif ($char === $quoteChar) {
227+
$inQuote = false;
228+
}
229+
}
230+
231+
// Parentheses management (only if not inside quotes)
232+
if (!$inQuote) {
233+
if ($char === '(') {
234+
$stack++;
235+
}
236+
if ($char === ')') {
237+
$stack--;
238+
}
239+
}
240+
241+
// Split by dot only at the root level (not in quotes/parentheses)
242+
if ($char === '.' && $stack === 0 && !$inQuote) {
243+
$parts[] = trim($buffer);
244+
$buffer = '';
245+
} else {
246+
$buffer .= $char;
247+
}
248+
}
249+
250+
if ($buffer !== '') {
251+
$parts[] = trim($buffer);
252+
}
253+
254+
return $parts;
255+
}
256+
}

0 commit comments

Comments
 (0)