Skip to content

Commit 5c80640

Browse files
committed
Fix nested @partial-block error with runtime partials
Also fixed failover rendering when a template contains a conditional call to a missing partial prior to calling it with block syntax and failover content.
1 parent 4b57ac4 commit 5c80640

5 files changed

Lines changed: 171 additions & 205 deletions

File tree

src/Compiler.php

Lines changed: 31 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,9 @@ public function compile(Program $program, Context $context): string
7878
*/
7979
public function composePHPRender(string $code): string
8080
{
81-
$runtime = Runtime::class;
8281
$partials = implode(",\n", $this->context->partialCode);
8382
$closure = self::templateClosure($code, $partials, "\n \$in = &\$cx->data['root'];");
84-
return "use {$runtime} as LR;\nreturn $closure;";
83+
return "use " . Runtime::class . " as LR;\nreturn $closure;";
8584
}
8685

8786
/**
@@ -274,7 +273,7 @@ private function compileProgramWithBlockParams(Program $program): string
274273
if ($bp) {
275274
array_shift($this->blockParamValues);
276275
}
277-
return self::blockClosure($body, (bool) $program->blockParams, $this->lastCompileProgramHadDirectBpRef);
276+
return self::blockClosure($body, (bool) $bp, $this->lastCompileProgramHadDirectBpRef);
278277
}
279278

280279
private function compileBlockHelper(BlockStatement $block, string $name): string
@@ -422,42 +421,33 @@ private function PartialStatement(PartialStatement $statement): string
422421
// appendContent opcode) and invoke the partial with an empty indent so its lines are
423422
// not additionally indented.
424423
if ($this->context->options->preventIndent && $statement->indent !== '') {
425-
return "$indent." . self::getRuntimeFunc('p', "\$cx, $p, $vars, 0, ''");
424+
return "$indent." . self::getRuntimeFunc('p', "\$cx, $p, $vars, ''");
426425
}
427426

428-
return self::getRuntimeFunc('p', "\$cx, $p, $vars, 0, $indent");
427+
return self::getRuntimeFunc('p', "\$cx, $p, $vars, $indent");
429428
}
430429

431430
private function PartialBlockStatement(PartialBlockStatement $statement): string
432431
{
433-
$this->context->partialBlockId++;
434-
$pid = $this->context->partialBlockId;
435-
436432
// Hoist inline partial registrations so they run before the partial is called.
437433
// Without this, inline partials defined in the block would only be registered when
438434
// {{> @partial-block}} is invoked, too late for partials that call them directly.
439-
$hoistedParts = [];
435+
$parts = [];
440436
foreach ($statement->program->body as $stmt) {
441437
if ($stmt instanceof BlockStatement && $stmt->type === 'DecoratorBlock') {
442-
$hoistedParts[] = $this->accept($stmt);
438+
$parts[] = $this->accept($stmt);
443439
}
444440
}
445441

446442
$name = $statement->name;
447443
$body = $this->compileProgram($statement->program);
444+
$partialName = null;
445+
$found = false;
448446

449447
if ($name instanceof PathExpression || $name instanceof StringLiteral || $name instanceof NumberLiteral) {
450448
$partialName = $this->resolvePartialName($name);
451449
$p = self::quote($partialName);
452-
} else {
453-
$p = $this->compileExpression($name);
454-
$partialName = null;
455-
}
456-
457-
$found = false;
458-
459-
if ($partialName !== null) {
460-
$found = isset($this->context->usedPartial[$partialName]);
450+
$found = ($this->context->usedPartial[$partialName] ?? '') !== '';
461451

462452
if (!$found && !str_starts_with($partialName, '@partial-block')) {
463453
$cnt = $this->resolvePartial($partialName);
@@ -468,26 +458,23 @@ private function PartialBlockStatement(PartialBlockStatement $statement): string
468458
}
469459
}
470460

471-
if (!$found) {
472-
// Mark as known so LR::p() can resolve it at runtime.
473-
$this->context->usedPartial[$partialName] = '';
474-
// Don't add to partialCode — register via LR::in() at runtime so $blockParams
475-
// is captured from the enclosing scope when block params are in use.
476-
}
461+
// Mark as known for runtime resolution; not added to partialCode so $blockParams scope is preserved.
462+
$this->context->usedPartial[$partialName] ??= '';
463+
} else {
464+
$p = $this->compileExpression($name);
477465
}
478466

479467
$vars = $this->compilePartialParams($statement->params, $statement->hash);
480468

481469
// Capture $blockParams if we're inside a block-param scope so the partial block body can access them.
482470
$useVars = $this->blockParamsUseVars();
483471
$bodyClosure = self::templateClosure($body, useVars: $useVars);
484-
$fallbackParts = ($partialName !== null && !$found)
485-
? [self::getRuntimeFunc('inFallback', "\$cx, " . self::quote($partialName) . ', ' . $bodyClosure)]
486-
: [];
487-
$parts = [...$hoistedParts, ...$fallbackParts,
488-
self::getRuntimeFunc('in', "\$cx, '@partial-block$pid', " . $bodyClosure),
489-
self::getRuntimeFunc('p', "\$cx, $p, $vars, $pid, ''"),
490-
];
472+
473+
if ($partialName !== null && !$found) {
474+
// Register the block body as a fallback partial only if no runtime partial with this name exists yet.
475+
$parts[] = "(isset(\$cx->partials[$p]) ? '' : " . self::getRuntimeFunc('in', "\$cx, $p, $bodyClosure") . ')';
476+
}
477+
$parts[] = self::getRuntimeFunc('p', "\$cx, $p, $vars, '', $bodyClosure");
491478
return implode('.', $parts);
492479
}
493480

@@ -617,7 +604,7 @@ private function PathExpression(PathExpression $expression): string
617604

618605
// @partial-block as variable: truthy when an active partial block exists
619606
if ($data && $depth === 0 && count($stringParts) === 1 && $stringParts[0] === 'partial-block') {
620-
return "isset(\$cx->partials['@partial-block' . \$cx->partialId]) ? true : null";
607+
return "\$cx->partialBlock !== null ? true : null";
621608
}
622609

623610
// Check block params (depth-0, non-data, non-scoped paths only, not SubExpression-headed)
@@ -788,13 +775,8 @@ private function compilePartialTemplate(string $name, string $template): void
788775
return;
789776
}
790777

791-
$tmpContext = clone $this->context;
792-
$tmpContext->inlinePartial = [];
793-
$tmpContext->partialBlock = [];
794-
795778
$program = $this->parser->parse($template);
796-
$code = (new Compiler($this->parser))->compile($program, $tmpContext);
797-
$this->context->merge($tmpContext);
779+
$code = (new Compiler($this->parser))->compile($program, $this->context);
798780

799781
$this->context->partialCode[$name] = self::quote($name) . ' => ' . self::templateClosure($code);
800782
}
@@ -897,13 +879,10 @@ private function compileElseClause(BlockStatement $block): string
897879
*/
898880
private function buildBasePath(bool $data, int $depth): string
899881
{
900-
$base = $data ? '$cx->frame' : '$in';
901-
if ($depth > 0) {
902-
$base = $data
903-
? $base . str_repeat("['_parent']", $depth)
904-
: "\$cx->depths[count(\$cx->depths)-$depth]";
882+
if ($data) {
883+
return '$cx->frame' . str_repeat("['_parent']", $depth);
905884
}
906-
return $base;
885+
return $depth > 0 ? "\$cx->depths[count(\$cx->depths)-$depth]" : '$in';
907886
}
908887

909888
/**
@@ -963,14 +942,13 @@ private static function blockClosure(string $body, bool $declaresBp = false, boo
963942
$preamble = '$sc=count($cx->depths);';
964943
$body = str_replace('$cx->depths[count($cx->depths)-', '$cx->depths[$sc-', $body);
965944
}
966-
if ($declaresBp) {
967-
return "function(\$cx, \$in, array \$blockParams = []) {{$preamble}return $body;}";
968-
}
969-
if ($inheritsBp) {
970-
// Inherits block params from the enclosing closure's $blockParams variable.
971-
return "function(\$cx, \$in) use (\$blockParams) {{$preamble}return $body;}";
972-
}
973-
return "function(\$cx, \$in) {{$preamble}return $body;}";
945+
// Inherits block params from the enclosing closure's $blockParams variable when $inheritsBp.
946+
$sig = match (true) {
947+
$declaresBp => "function(\$cx, \$in, array \$blockParams = [])",
948+
$inheritsBp => "function(\$cx, \$in) use (\$blockParams)",
949+
default => "function(\$cx, \$in)",
950+
};
951+
return "$sig {{$preamble}return $body;}";
974952
}
975953

976954
private static function quote(string $string): string

src/Context.php

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,14 @@ final class Context
1111
* @param array<string, string> $usedPartial
1212
* @param array<string, string> $partialCode
1313
* @param array<string, string> $partials
14-
* @param array<mixed> $partialBlock
15-
* @param array<mixed> $inlinePartial
1614
*/
1715
public function __construct(
1816
public readonly Options $options,
1917
public array $usedPartial = [],
2018
public array $partialCode = [],
2119
public int $usedDynPartial = 0,
22-
public int $usedPBlock = 0,
23-
public int $partialBlockId = 0,
2420
public array $partials = [],
25-
public array $partialBlock = [],
26-
public array $inlinePartial = [],
2721
) {
2822
$this->partials = $options->partials;
2923
}
30-
31-
/**
32-
* Update from another context.
33-
*/
34-
public function merge(self $context): void
35-
{
36-
$this->partials = $context->partials;
37-
$this->partialCode = $context->partialCode;
38-
$this->usedDynPartial = $context->usedDynPartial;
39-
$this->usedPBlock = $context->usedPBlock;
40-
$this->partialBlockId = $context->partialBlockId;
41-
$this->usedPartial = $context->usedPartial;
42-
}
4324
}

src/Runtime.php

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ public static function createContext(mixed $context, array $options, array $comp
166166
partials: $parentCx->partials,
167167
depths: $parentCx->depths,
168168
data: $root,
169-
partialId: $parentCx->partialId,
170169
frame: $parentCx->frame,
170+
partialBlock: $parentCx->partialBlock,
171171
);
172172
}
173173

@@ -366,17 +366,28 @@ public static function merge(mixed $a, mixed $b): mixed
366366
* @param array<string, mixed> $hash named hash overrides merged into the context
367367
* @param string $indent whitespace to prepend to each line of the partial's output
368368
*/
369-
public static function p(RuntimeContext $cx, string $name, mixed $context, array $hash, int $pid, string $indent): string
369+
public static function p(RuntimeContext $cx, string $name, mixed $context, array $hash, string $indent, ?\Closure $partialBlock = null): string
370370
{
371-
$pp = ($name === '@partial-block') ? $name . ($pid > 0 ? $pid : $cx->partialId) : $name;
372-
373-
$fn = $cx->partials[$pp] ?? null;
371+
$fn = $name === '@partial-block' ? $cx->partialBlock : ($cx->partials[$name] ?? null);
374372
if ($fn === null) {
375373
throw new \Exception("The partial $name could not be found");
376374
}
377375

378-
$savedPartialId = $cx->partialId;
379-
$cx->partialId = ($name === '@partial-block') ? ($pid > 0 ? $pid : ($cx->partialId > 0 ? $cx->partialId - 1 : 0)) : $pid;
376+
// Install a wrapper as the active @partial-block so the partial can invoke it via {{> @partial-block}}.
377+
// The wrapper temporarily restores the previously active block before calling $partialBlock,
378+
// allowing nested partial blocks to correctly resolve their own @partial-block.
379+
if ($partialBlock !== null) {
380+
$currentBlock = $cx->partialBlock;
381+
$cx->partialBlock = static function (mixed $blockContext) use ($partialBlock, $currentBlock): string {
382+
$callingCx = self::$partialContext;
383+
assert($callingCx !== null);
384+
$saved = $callingCx->partialBlock;
385+
$callingCx->partialBlock = $currentBlock;
386+
$result = $partialBlock($blockContext);
387+
$callingCx->partialBlock = $saved;
388+
return $result;
389+
};
390+
}
380391

381392
$context = $hash ? static::merge($context, $hash) : $context;
382393
$prev = self::$partialContext;
@@ -385,8 +396,10 @@ public static function p(RuntimeContext $cx, string $name, mixed $context, array
385396
$result = $fn($context);
386397
} finally {
387398
self::$partialContext = $prev;
399+
if ($partialBlock !== null) {
400+
$cx->partialBlock = $currentBlock;
401+
}
388402
}
389-
$cx->partialId = $savedPartialId;
390403

391404
if ($indent !== '') {
392405
$lines = explode("\n", $result);
@@ -405,43 +418,14 @@ public static function p(RuntimeContext $cx, string $name, mixed $context, array
405418
}
406419

407420
/**
408-
* Like in() but only registers the partial if it is not already present in the context.
409-
* Used for {{#> partial}}fallback{{/partial}} blocks when the partial was not found at compile time.
410-
*
411-
* @param string $name partial name
412-
* @param \Closure $partial the compiled fallback partial
413-
*/
414-
public static function inFallback(RuntimeContext $cx, string $name, \Closure $partial): string
415-
{
416-
$cx->partials[$name] ??= $partial;
417-
return '';
418-
}
419-
420-
/**
421-
* For {{#* inlinepartial}} .
421+
* For {{#* inline "name"}} and {{#> partial}}fallback{{/partial}} blocks.
422422
*
423423
* @param string $name partial name
424424
* @param \Closure $partial the compiled partial
425425
*/
426426
public static function in(RuntimeContext $cx, string $name, \Closure $partial): string
427427
{
428-
if (str_starts_with($name, '@partial-block')) {
429-
// Capture the outer partialId at registration time so that when this
430-
// block closure runs, any {{>@partial-block}} inside it resolves to
431-
// the correct outer partial block rather than following the pid-decrement chain.
432-
$outerPartialId = $cx->partialId;
433-
$cx->partials[$name] = function (mixed $context = null, array $options = []) use ($partial, $outerPartialId): string {
434-
$callingCx = self::$partialContext;
435-
assert($callingCx !== null);
436-
$savedId = $callingCx->partialId;
437-
$callingCx->partialId = $outerPartialId;
438-
$result = $partial($context, $options);
439-
$callingCx->partialId = $savedId;
440-
return $result;
441-
};
442-
} else {
443-
$cx->partials[$name] = $partial;
444-
}
428+
$cx->partials[$name] = $partial;
445429
return '';
446430
}
447431

src/RuntimeContext.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function __construct(
1919
public array $partials = [],
2020
public array $depths = [],
2121
public array $data = [],
22-
public int $partialId = 0,
2322
public array $frame = [],
23+
public ?\Closure $partialBlock = null,
2424
) {}
2525
}

0 commit comments

Comments
 (0)