Skip to content

Commit 8dd5396

Browse files
authored
feat: add #blank, #solo, #inject, and #slot directives to Odo templat… (#231)
2 parents 644adb0 + 5316093 commit 8dd5396

File tree

3 files changed

+205
-19
lines changed

3 files changed

+205
-19
lines changed

src/Phaseolies/Http/Controllers/Controller.php

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,38 +108,34 @@ class Controller extends View
108108
*/
109109
protected const MAX_COMPILE_RETRIES = 3;
110110

111+
/**
112+
* Counter for tracking solo block uniqueness
113+
*
114+
* @var int
115+
*/
116+
protected int $soloCounter = 0;
117+
118+
/**
119+
* Tracks which solo blocks have already rendered
120+
*
121+
* @var array
122+
*/
123+
protected array $soloStack = [];
124+
111125
/**
112126
* Constructor to initialize the template engine with default settings
113127
*/
114128
public function __construct()
115129
{
116130
parent::__construct();
117-
118-
// Set the file extension for template files (changed to .odo.php)
119131
$this->setFileExtension('.odo.php');
120-
121-
// Set the directory where view files are stored
122132
$this->setViewFolder('resources/views' . DIRECTORY_SEPARATOR);
123-
124-
// Set the directory where cached files are stored
125133
$this->setCacheFolder('storage/framework/views' . DIRECTORY_SEPARATOR);
126-
127-
// Create the cache folder if it doesn't exist
128134
$this->createCacheFolder();
129-
130-
// Set the directory where cached files are stored
131135
$this->setSymlinkPathFolder('storage/app/public' . DIRECTORY_SEPARATOR);
132-
133-
// Create the cache folder if it doesn't exist
134136
$this->createPublicSymlinkFolder();
135-
136-
// Set the format for echoing variables in templates
137137
$this->setEchoFormat('$this->e(%s)');
138-
139-
// Initialize arrays for blocks, block stacks, and loop stacks
140138
$this->loopStacks = [];
141-
142-
// Load custom syntax from config if available
143139
$this->loadCustomSyntax();
144140
}
145141

src/Phaseolies/Support/Odo/OdoCondition.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,4 +541,111 @@ protected function compileEndscopenot(): string
541541
{
542542
return "<?php endif; ?>";
543543
}
544+
545+
/**
546+
* Usage: #blank($variable)
547+
* Renders block when variable is empty, null, or an empty array/string
548+
*
549+
* @param mixed $variable
550+
* @return string
551+
*/
552+
protected function compileBlank($variable): string
553+
{
554+
return "<?php if(empty{$variable}): ?>";
555+
}
556+
557+
/**
558+
* Usage: #endblank
559+
*
560+
* @return string
561+
*/
562+
protected function compileEndblank(): string
563+
{
564+
return "<?php endif; ?>";
565+
}
566+
567+
/**
568+
* Usage: #notblank($variable)
569+
* Renders block when variable is NOT empty
570+
*
571+
* @param mixed $variable
572+
* @return string
573+
*/
574+
protected function compileNotblank($variable): string
575+
{
576+
return "<?php if(!empty{$variable}): ?>";
577+
}
578+
579+
/**
580+
* Usage: #endnotblank
581+
*
582+
* @return string
583+
*/
584+
protected function compileEndnotblank(): string
585+
{
586+
return "<?php endif; ?>";
587+
}
588+
589+
/**
590+
* Usage: #solo
591+
* Renders its content only once, even if inside a loop
592+
*
593+
* @return string
594+
*/
595+
protected function compileSolo(): string
596+
{
597+
return "<?php if(!isset(\$__solo['" . ($this->soloCounter ?? $this->soloCounter = 0) . "'])): \$__solo['" . $this->soloCounter++ . "'] = true; ?>";
598+
}
599+
600+
/**
601+
* Usage: #endsolo
602+
*
603+
* @return string
604+
*/
605+
protected function compileEndsolo(): string
606+
{
607+
return "<?php endif; ?>";
608+
}
609+
610+
/**
611+
* Usage: #inject('stack-name')
612+
* Pushes content into a named stack
613+
*
614+
* @param string $expression
615+
* @return string
616+
*/
617+
protected function compileInject($expression): string
618+
{
619+
if (isset($expression[0]) && '(' === $expression[0]) {
620+
$expression = substr($expression, 1, -1);
621+
}
622+
623+
return "<?php \$this->startInject({$expression}); ?>";
624+
}
625+
626+
/**
627+
* Usage: #endinject
628+
*
629+
* @return string
630+
*/
631+
protected function compileEndinject(): string
632+
{
633+
return "<?php \$this->endInject(); ?>";
634+
}
635+
636+
/**
637+
* Usage: #slot('stack-name')
638+
* Outputs all content pushed into a named stack
639+
*
640+
* @param string $expression
641+
* @return string
642+
*/
643+
protected function compileSlot($expression): string
644+
{
645+
if (isset($expression[0]) && '(' === $expression[0]) {
646+
$expression = substr($expression, 1, -1);
647+
}
648+
649+
return "<?php echo \$this->renderSlot({$expression}); ?>";
650+
}
544651
}

src/Phaseolies/Support/View/View.php

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,25 @@ class View extends Factory
4444

4545
/**
4646
* Stack of currently rendering view names.
47-
* Useful for debugging and internal state tracking.
4847
*
4948
* @var array<int, string>
5049
*/
5150
protected $renderStack = [];
5251

52+
/**
53+
* Named stacks for inject/slot system
54+
*
55+
* @var array<string, array>
56+
*/
57+
protected array $stacks = [];
58+
59+
/**
60+
* Currently open inject stack name
61+
*
62+
* @var string|null
63+
*/
64+
protected ?string $currentInject = null;
65+
5366
public function __construct()
5467
{
5568
$this->factory = app('view');
@@ -262,6 +275,74 @@ private function isAssoc(array $array): bool
262275
return array_keys($array) !== range(0, count($array) - 1);
263276
}
264277

278+
/**
279+
* Start capturing content for a named inject stack
280+
*
281+
* @param string $name
282+
* @return void
283+
*/
284+
public function startInject(string $name): void
285+
{
286+
if (empty($name)) {
287+
throw new \InvalidArgumentException('Inject stack name must not be empty');
288+
}
289+
290+
$this->currentInject = trim($name, "'\"");
291+
ob_start();
292+
}
293+
294+
/**
295+
* End capturing and push content into the named stack
296+
*
297+
* @return void
298+
*/
299+
public function endInject(): void
300+
{
301+
if ($this->currentInject === null) {
302+
throw new \RuntimeException('Cannot end inject — no inject block is open');
303+
}
304+
305+
$content = ob_get_clean();
306+
$name = $this->currentInject;
307+
308+
if (!isset($this->stacks[$name])) {
309+
$this->stacks[$name] = [];
310+
}
311+
312+
$this->stacks[$name][] = $content;
313+
$this->currentInject = null;
314+
}
315+
316+
/**
317+
* Render all content pushed into a named stack
318+
*
319+
* @param string $name
320+
* @return string
321+
*/
322+
public function renderSlot(string $name): string
323+
{
324+
$name = trim($name, "'\"");
325+
326+
if (!isset($this->stacks[$name])) {
327+
return '';
328+
}
329+
330+
return implode('', $this->stacks[$name]);
331+
}
332+
333+
/**
334+
* Check if a stack has any content
335+
*
336+
* @param string $name
337+
* @return bool
338+
*/
339+
public function hasSlot(string $name): bool
340+
{
341+
$name = trim($name, "'\"");
342+
343+
return !empty($this->stacks[$name]);
344+
}
345+
265346
/**
266347
* Clean the block state
267348
*
@@ -272,6 +353,8 @@ public function flush()
272353
$this->blocks = [];
273354
$this->blockStacks = [];
274355
$this->parents = [];
356+
$this->stacks = [];
357+
$this->currentInject = null;
275358
}
276359

277360
public function __destruct()

0 commit comments

Comments
 (0)