Skip to content

Commit a4f48ec

Browse files
committed
Attempt to implement partial support (unfinished)
1 parent bd6b559 commit a4f48ec

8 files changed

Lines changed: 195 additions & 323 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"devtheorem/php-handlebars-parser": "dev-master"
2525
},
2626
"require-dev": {
27-
"friendsofphp/php-cs-fixer": "^3.93",
27+
"friendsofphp/php-cs-fixer": "^3.94",
2828
"jbboehr/handlebars-spec": "dev-master",
2929
"phpstan/phpstan": "^2.1.38",
3030
"phpunit/phpunit": "^11.5"

composer.lock

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Handlebars.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ public static function precompile_new(string $template, Options $options = new O
4747
$context = new Context($options);
4848
$parser = (new ParserFactory())->create();
4949
$program = $parser->parse($template);
50-
$code = (new NewCompiler())->compile($program, $context);
50+
$compiler = new NewCompiler();
51+
$code = $compiler->compile($program, $context);
52+
$compiler->handleDynamicPartials();
53+
static::handleError($context);
5154

5255
// return full PHP render code as string
5356
return Compiler::composePHPRender($context, $code);

src/NewCompiler.php

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use DevTheorem\HandlebarsParser\Ast\StringLiteral;
2222
use DevTheorem\HandlebarsParser\Ast\SubExpression;
2323
use DevTheorem\HandlebarsParser\Ast\UndefinedLiteral;
24+
use DevTheorem\HandlebarsParser\ParserFactory;
2425

2526
/**
2627
* @internal
@@ -76,7 +77,7 @@ private function compileProgram(Program $program, bool $withSp = false): string
7677
private function accept(Node $node): string
7778
{
7879
return match (true) {
79-
$node instanceof BlockStatement && $node->type === 'DecoratorBlock' => $this->DecoratorBlock(),
80+
$node instanceof BlockStatement && $node->type === 'DecoratorBlock' => $this->DecoratorBlock($node),
8081
$node instanceof BlockStatement => $this->BlockStatement($node),
8182
$node instanceof PartialStatement => $this->PartialStatement($node),
8283
$node instanceof PartialBlockStatement => $this->PartialBlockStatement($node),
@@ -268,9 +269,23 @@ private function compileBlockHelper(BlockStatement $block, string $helperName):
268269
return "'." . $this->getFuncName('hbbch', "\$cx, '$helperName', $params, \$in, false, function(\$cx, \$in) {return $body;}$else") . ").'";
269270
}
270271

271-
private function DecoratorBlock(): string
272+
private function DecoratorBlock(BlockStatement $block): string
272273
{
273-
return ''; // todo: throw?
274+
$helperName = $this->getSimpleHelperName($block->path);
275+
276+
if ($helperName !== 'inline' || count($block->params) !== 1 || !$block->params[0] instanceof StringLiteral) {
277+
return '';
278+
}
279+
280+
$partialName = $block->params[0]->value;
281+
$body = $block->program ? $this->compileProgram($block->program, true) : "''";
282+
283+
// Register at compile time so {{> partialName}} can find it
284+
$func = "function (\$cx, \$in, \$sp) {{$this->context->fStart}$body{$this->context->fEnd}}";
285+
$this->context->usedPartial[$partialName] = '';
286+
$this->context->partialCode[$partialName] = Expression::quoteString($partialName) . " => $func";
287+
288+
return "'." . $this->getFuncName('in', "\$cx, '" . addcslashes($partialName, "'\\") . "', function(\$cx, \$in, \$sp) {return $body;}") . ").'";
274289
}
275290

276291
private function Decorator(Decorator $decorator): string
@@ -284,8 +299,10 @@ private function PartialStatement(PartialStatement $statement): string
284299

285300
if ($name instanceof PathExpression) {
286301
$p = "'" . addcslashes($name->original, "'\\") . "'";
302+
$this->resolveAndCompilePartial($name->original);
287303
} elseif ($name instanceof SubExpression) {
288304
$p = $this->SubExpression($name);
305+
$this->context->usedDynPartial++;
289306
} else {
290307
$p = $this->compileExpression($name);
291308
}
@@ -303,14 +320,37 @@ private function PartialBlockStatement(PartialBlockStatement $statement): string
303320
$pid = $this->partialBlockId;
304321

305322
$name = $statement->name;
323+
$body = $this->compileProgram($statement->program, true);
324+
$found = false;
306325

307326
if ($name instanceof PathExpression) {
308327
$p = "'" . addcslashes($name->original, "'\\") . "'";
328+
$partialName = $name->original;
329+
330+
if (!isset($this->context->usedPartial[$partialName])
331+
&& !str_starts_with($partialName, '@partial-block')
332+
) {
333+
$resolveName = $partialName;
334+
$cnt = Partial::resolve($this->context, $resolveName);
335+
if ($cnt !== null) {
336+
$this->context->usedPartial[$resolveName] = $cnt;
337+
$this->compilePartialTemplate($resolveName, $cnt);
338+
$found = true;
339+
}
340+
} else {
341+
$found = isset($this->context->usedPartial[$partialName]);
342+
}
343+
344+
if (!$found) {
345+
// Register fallback body as the partial
346+
$func = "function (\$cx, \$in, \$sp) {{$this->context->fStart}$body{$this->context->fEnd}}";
347+
$this->context->usedPartial[$partialName] = '';
348+
$this->context->partialCode[$partialName] = Expression::quoteString($partialName) . " => $func";
349+
}
309350
} else {
310351
$p = $this->compileExpression($name);
311352
}
312353

313-
$body = $this->compileProgram($statement->program, true);
314354
$vars = $this->compilePartialParams($statement->params, $statement->hash);
315355
$sp = "(\$sp ?? '') . ''";
316356

@@ -454,8 +494,8 @@ private function PathExpression(PathExpression $expression): string
454494
if ($p !== '' && $depth === 0) {
455495
$checks[] = "isset($base$p)";
456496
}
457-
$baseP = "$base$p";
458-
$checks[] = $baseP === '$in' ? '$inary' : "is_array($base$p)";
497+
$baseP = "$base$p";
498+
$checks[] = $baseP === '$in' ? '$inary' : "is_array($base$p)";
459499

460500
$cond = implode(' && ', $checks);
461501
if (count($checks) > 1) {
@@ -506,6 +546,77 @@ private function Hash(Hash $hash): string
506546
return implode(',', $pairs);
507547
}
508548

549+
// ── Partials ─────────────────────────────────────────────────────
550+
551+
private function resolveAndCompilePartial(string $name): void
552+
{
553+
if (isset($this->context->usedPartial[$name])) {
554+
return;
555+
}
556+
557+
// @partial-block is resolved at runtime via LR::in()/LR::p()
558+
if (str_starts_with($name, '@partial-block')) {
559+
return;
560+
}
561+
562+
$cnt = Partial::resolve($this->context, $name);
563+
564+
if ($cnt !== null) {
565+
$this->context->usedPartial[$name] = $cnt;
566+
$this->compilePartialTemplate($name, $cnt);
567+
return;
568+
}
569+
570+
$this->context->error[] = "The partial $name could not be found";
571+
}
572+
573+
private function compilePartialTemplate(string $name, string $template): void
574+
{
575+
if (isset($this->context->partialCode[$name])) {
576+
return;
577+
}
578+
579+
// Prevent infinite recursion
580+
if (end($this->context->partialStack) === $name && str_starts_with($name, '@partial-block')) {
581+
return;
582+
}
583+
584+
$tmpContext = clone $this->context;
585+
$tmpContext->inlinePartial = [];
586+
$tmpContext->partialBlock = [];
587+
$tmpContext->partialStack[] = $name;
588+
589+
$program = (new ParserFactory())->create()->parse($template);
590+
$code = (new NewCompiler())->compile($program, $tmpContext);
591+
$this->context->merge($tmpContext);
592+
593+
if (!$this->context->options->preventIndent) {
594+
$code = preg_replace('/^/m', "'{$this->context->separator}\$sp{$this->context->separator}'", $code);
595+
// remove extra spaces before partial
596+
$code = preg_replace('/^\'\\.\\$sp\\.\'(\'\\.LR::p\\()/m', '$1', $code, 1);
597+
// remove extra spaces before section
598+
$code = preg_replace('/^\'\\.\\$sp\\.\'(\'\\.LR::sec\\()/m', '$1', $code, 1);
599+
// remove extra spaces before blank lines
600+
$code = preg_replace('/^\'\\.\\$sp\\.\'(\';}\\))/m', '$1', $code, 1);
601+
// add spaces after partial
602+
$code = preg_replace('/^(\'\\.LR::p\\(.+\\)\\.)(\'.+)/m', '$1\$sp.$2', $code, 1);
603+
}
604+
605+
$func = "function (\$cx, \$in, \$sp) {{$this->context->fStart}'$code'{$this->context->fEnd}}";
606+
$this->context->partialCode[$name] = Expression::quoteString($name) . " => $func";
607+
}
608+
609+
public function handleDynamicPartials(): void
610+
{
611+
if ($this->context->usedDynPartial === 0) {
612+
return;
613+
}
614+
615+
foreach ($this->context->partials as $name => $code) {
616+
$this->resolveAndCompilePartial($name);
617+
}
618+
}
619+
509620
// ── Helpers ──────────────────────────────────────────────────────
510621

511622
/**
@@ -535,7 +646,7 @@ private function compileParams(array $params, ?Hash $hash, ?array $blockParams =
535646
private function compilePartialParams(array $params, ?Hash $hash): string
536647
{
537648
if (!$params) {
538-
$contextVar = $this->context->options->explicitPartialContext ? 'null' : '';
649+
$contextVar = $this->context->options->explicitPartialContext ? 'null' : '$in';
539650
} else {
540651
$contextVar = $this->compileExpression($params[0]);
541652
}

src/Runtime.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,11 @@ public static function p(RuntimeContext $cx, string $p, $v, int $pid, string $sp
300300

301301
$cx = clone $cx;
302302
$cx->partialId = ($p === '@partial-block') ? ($pid > 0 ? $pid : ($cx->partialId > 0 ? $cx->partialId - 1 : 0)) : $pid;
303+
$cx->partialDepth++;
304+
305+
if ($cx->partialDepth > 100) {
306+
throw new \Exception("Runtime: the partial $p could not be found");
307+
}
303308

304309
return $cx->partials[$pp]($cx, static::merge($v[0][0], $v[1]), $sp);
305310
}

src/RuntimeContext.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ public function __construct(
2121
public array $spVars = [],
2222
public array $blParam = [],
2323
public int $partialId = 0,
24+
public int $partialDepth = 0,
2425
) {}
2526
}

0 commit comments

Comments
 (0)