Skip to content

Commit 877daba

Browse files
committed
Refactor duplicated logic to methods
1 parent 2624ba5 commit 877daba

File tree

2 files changed

+93
-63
lines changed

2 files changed

+93
-63
lines changed

src/Compiler.php

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -170,15 +170,13 @@ private function BlockStatement(BlockStatement $block): string
170170
return $this->compileBlockHelper($block, $literalKey);
171171
}
172172

173-
$escapedKey = addcslashes($literalKey, "'\\");
174-
$miss = $this->context->options->strict
175-
? "LR::miss('" . addcslashes($literalKey, "'\\") . "')"
176-
: 'null';
173+
$escapedKey = $this->escape($literalKey);
174+
$miss = $this->missValue($literalKey);
177175
$var = "\$in['$escapedKey'] ?? $miss";
178176

179177
if ($block->program === null) {
180178
// Inverted section: {{^"foo"}}...{{/"foo"}}
181-
$body = $block->inverse ? $this->compileProgram($block->inverse) : "''";
179+
$body = $this->compileProgramOrEmpty($block->inverse);
182180
return "'.((" . $this->getFuncName('isec', $var) . ")) ? $body : '').'";
183181
}
184182

@@ -240,7 +238,7 @@ private function compileIf(BlockStatement $block, bool $unless): string
240238
$var = $this->compileExpression($block->params[0]);
241239
$includeZero = $this->getIncludeZero($block->hash);
242240

243-
$then = $block->program ? $this->compileProgram($block->program) : "''";
241+
$then = $this->compileProgramOrEmpty($block->program);
244242

245243
if ($block->inverse && $block->inverse->chained) {
246244
// {{else if ...}} chain — compile the inner block directly
@@ -250,7 +248,7 @@ private function compileIf(BlockStatement $block, bool $unless): string
250248
}
251249
$else = "'" . $elseCode . "'";
252250
} else {
253-
$else = $block->inverse ? $this->compileProgram($block->inverse) : "''";
251+
$else = $this->compileProgramOrEmpty($block->inverse);
254252
}
255253

256254
$negate = $unless ? '!' : '';
@@ -264,8 +262,7 @@ private function compileEach(BlockStatement $block): string
264262
}
265263

266264
$var = $this->compileExpression($block->params[0]);
267-
$bp = $block->program ? $block->program->blockParams : [];
268-
$bs = $bp ? Expression::listString($bp) : 'null';
265+
[$bp, $bs] = $this->getProgramBlockParams($block->program);
269266

270267
$body = $block->program ? $this->compileProgramWithBlockParams($block->program, $bp, true) : "''";
271268
$else = $this->compileElseClause($block);
@@ -280,10 +277,9 @@ private function compileWith(BlockStatement $block): string
280277
}
281278

282279
$var = $this->compileExpression($block->params[0]);
283-
$bp = $block->program ? $block->program->blockParams : [];
284-
$bs = $bp ? Expression::listString($bp) : 'null';
280+
[$bp, $bs] = $this->getProgramBlockParams($block->program);
285281

286-
$body = $block->program ? $this->compileProgram($block->program) : "''";
282+
$body = $this->compileProgramOrEmpty($block->program);
287283
$else = $this->compileElseClause($block);
288284

289285
return "'." . $this->getFuncName('wi', "\$cx, LR::dv($var, \$in), $bs, \$in, function(\$cx, \$in) {return $body;}$else") . ").'";
@@ -293,7 +289,7 @@ private function compileSection(BlockStatement $block): string
293289
{
294290
$var = $this->compileExpression($block->path);
295291

296-
$body = $block->program ? $this->compileProgram($block->program, true) : "''";
292+
$body = $this->compileProgramOrEmpty($block->program, true);
297293
$else = $this->compileElseClause($block);
298294

299295
return "'." . $this->getFuncName('sec', "\$cx, $var, null, \$in, false, function(\$cx, \$in) use (&\$sp) {return $body;}$else") . ").'";
@@ -302,7 +298,7 @@ private function compileSection(BlockStatement $block): string
302298
private function compileInvertedSection(BlockStatement $block): string
303299
{
304300
$var = $this->compileExpression($block->path);
305-
$body = $block->inverse ? $this->compileProgram($block->inverse) : "''";
301+
$body = $this->compileProgramOrEmpty($block->inverse);
306302

307303
return "'.((" . $this->getFuncName('isec', $var) . ")) ? $body : '').'";
308304
}
@@ -321,7 +317,7 @@ private function compileBlockHelper(BlockStatement $block, string $helperName):
321317
$params = $this->compileParams($block->params, $block->hash, $bp ?: null);
322318

323319
if ($inverted) {
324-
$body = $block->inverse ? $this->compileProgram($block->inverse) : "''";
320+
$body = $this->compileProgramOrEmpty($block->inverse);
325321
return "'." . $this->getFuncName('hbbch', "\$cx, '$helperName', $params, \$in, true, function(\$cx, \$in) {return $body;}") . ").'";
326322
}
327323

@@ -347,13 +343,13 @@ private function DecoratorBlock(BlockStatement $block): string
347343
$partialName = $this->getLiteralKeyName($firstArg);
348344
}
349345

350-
$body = $block->program ? $this->compileProgram($block->program, true) : "''";
346+
$body = $this->compileProgramOrEmpty($block->program, true);
351347

352348
// Register in usedPartial so {{> partialName}} can compile without error.
353349
// Do NOT add to partialCode - `in()` handles runtime registration, keeping inline partials block-scoped.
354350
$this->context->usedPartial[$partialName] = '';
355351

356-
return "'." . $this->getFuncName('in', "\$cx, '" . addcslashes($partialName, "'\\") . "', function(\$cx, \$in, \$sp) {return $body;}") . ").'";
352+
return "'." . $this->getFuncName('in', "\$cx, '" . $this->escape($partialName) . "', function(\$cx, \$in, \$sp) {return $body;}") . ").'";
357353
}
358354

359355
private function Decorator(Decorator $decorator): never
@@ -366,25 +362,21 @@ private function PartialStatement(PartialStatement $statement): string
366362
$name = $statement->name;
367363

368364
if ($name instanceof PathExpression) {
369-
$p = "'" . addcslashes($name->original, "'\\") . "'";
365+
$p = "'" . $this->escape($name->original) . "'";
370366
$this->resolveAndCompilePartial($name->original);
371367
} elseif ($name instanceof SubExpression) {
372368
$p = $this->SubExpression($name);
373369
$this->context->usedDynPartial++;
374-
} elseif ($name instanceof NumberLiteral) {
375-
$literalName = (string) $name->value;
376-
$p = "'" . addcslashes($literalName, "'\\") . "'";
377-
$this->resolveAndCompilePartial($literalName);
378-
} elseif ($name instanceof StringLiteral) {
379-
$literalName = $name->value;
380-
$p = "'" . addcslashes($literalName, "'\\") . "'";
370+
} elseif ($name instanceof NumberLiteral || $name instanceof StringLiteral) {
371+
$literalName = $this->getLiteralKeyName($name);
372+
$p = "'" . $this->escape($literalName) . "'";
381373
$this->resolveAndCompilePartial($literalName);
382374
} else {
383375
$p = $this->compileExpression($name);
384376
}
385377

386378
$vars = $this->compilePartialParams($statement->params, $statement->hash);
387-
$indent = addcslashes($statement->indent, "'\\");
379+
$indent = $this->escape($statement->indent);
388380

389381
// When preventIndent is set, emit the indent as literal content (like handlebars.js
390382
// appendContent opcode) and invoke the partial with an empty indent so its lines are
@@ -418,10 +410,10 @@ private function PartialBlockStatement(PartialBlockStatement $statement): string
418410

419411
if ($name instanceof PathExpression) {
420412
$partialName = $name->original;
421-
$p = "'" . addcslashes($partialName, "'\\") . "'";
413+
$p = "'" . $this->escape($partialName) . "'";
422414
} elseif ($name instanceof StringLiteral || $name instanceof NumberLiteral) {
423-
$partialName = $name instanceof StringLiteral ? $name->value : (string) $name->value;
424-
$p = "'" . addcslashes($partialName, "'\\") . "'";
415+
$partialName = $this->getLiteralKeyName($name);
416+
$p = "'" . $this->escape($partialName) . "'";
425417
} else {
426418
$p = $this->compileExpression($name);
427419
$partialName = null;
@@ -509,7 +501,7 @@ private function MustacheStatement(MustacheStatement $mustache): string
509501

510502
if ($this->resolveHelper($literalKey)) {
511503
$params = $this->compileParams($mustache->params, $mustache->hash);
512-
$escapedKey = addcslashes($literalKey, "'\\");
504+
$escapedKey = $this->escape($literalKey);
513505
$call = "LR::hbch(\$cx, '$escapedKey', $params, \$in)";
514506
return "'." . $this->getFuncName($fn, $call) . ").'";
515507
}
@@ -518,17 +510,15 @@ private function MustacheStatement(MustacheStatement $mustache): string
518510
throw new \Exception('Missing helper: "' . $literalKey . '"');
519511
}
520512

521-
$escapedKey = addcslashes($literalKey, "'\\");
522-
$miss = $this->context->options->strict
523-
? "LR::miss('" . addcslashes($literalKey, "'\\") . "')"
524-
: 'null';
513+
$escapedKey = $this->escape($literalKey);
514+
$miss = $this->missValue($literalKey);
525515
$val = "\$in['$escapedKey'] ?? $miss";
526516
return "'." . $this->getFuncName($fn, $val) . ").'";
527517
}
528518

529519
private function ContentStatement(ContentStatement $statement): string
530520
{
531-
return addcslashes($statement->value, "'\\");
521+
return $this->escape($statement->value);
532522
}
533523

534524
private function CommentStatement(CommentStatement $statement): string
@@ -552,7 +542,7 @@ private function SubExpression(SubExpression $expression): string
552542
// Registered helper
553543
if ($helperName !== null && $this->resolveHelper($helperName)) {
554544
$params = $this->compileParams($expression->params, $expression->hash);
555-
$escapedName = addcslashes($helperName, "'\\");
545+
$escapedName = $this->escape($helperName);
556546
return "LR::hbch(\$cx, '$escapedName', $params, \$in)";
557547
}
558548

@@ -577,16 +567,14 @@ private function PathExpression(PathExpression $expression): string
577567
$base = $this->buildBasePath($data, $depth);
578568

579569
// Filter out SubExpression parts for string-only operations
580-
$stringParts = array_values(array_filter($parts, fn($p) => is_string($p)));
570+
$stringParts = self::stringPartsOf($parts);
581571

582572
// `this` with no parts or empty parts
583573
if (($expression->this_ && !$parts) || !$stringParts) {
584574
return $base;
585575
}
586576

587-
$miss = $this->context->options->strict
588-
? "LR::miss('" . addcslashes($expression->original, "'\\") . "')"
589-
: 'null';
577+
$miss = $this->missValue($expression->original);
590578

591579
// @partial-block as variable: truthy when an active partial block exists
592580
if ($data && $depth === 0 && count($stringParts) === 1 && $stringParts[0] === 'partial-block') {
@@ -597,7 +585,7 @@ private function PathExpression(PathExpression $expression): string
597585
if (!$data && $depth === 0 && !self::scopedId($expression)) {
598586
$bpIdx = $this->lookupBlockParam($stringParts[0]);
599587
if ($bpIdx !== null) {
600-
$escapedName = addcslashes($stringParts[0], "'\\");
588+
$escapedName = $this->escape($stringParts[0]);
601589
$bpBase = "\$cx->blParam[$bpIdx]['$escapedName']";
602590
$remaining = $this->buildKeyAccess(array_slice($stringParts, 1));
603591
return "$bpBase$remaining ?? $miss";
@@ -638,7 +626,7 @@ private function PathExpression(PathExpression $expression): string
638626

639627
private function StringLiteral(StringLiteral $literal): string
640628
{
641-
return "'" . addcslashes($literal->value, "'\\") . "'";
629+
return "'" . $this->escape($literal->value) . "'";
642630
}
643631

644632
private function NumberLiteral(NumberLiteral $literal): string
@@ -700,7 +688,7 @@ private function Hash(Hash $hash): string
700688
{
701689
$pairs = [];
702690
foreach ($hash->pairs as $pair) {
703-
$key = addcslashes($pair->key, "'\\");
691+
$key = $this->escape($pair->key);
704692
$value = $this->compileExpression($pair->value);
705693
$pairs[] = "'$key'=>$value";
706694
}
@@ -905,7 +893,7 @@ private function buildKeyAccess(array $parts): string
905893
{
906894
$n = '';
907895
foreach ($parts as $part) {
908-
$n .= "['" . addcslashes($part, "'\\") . "']";
896+
$n .= "['" . $this->escape($part) . "']";
909897
}
910898
return $n;
911899
}
@@ -923,6 +911,41 @@ private function getFuncName(string $name, string $args): string
923911
return "LR::$name($args";
924912
}
925913

914+
private function escape(string $s): string
915+
{
916+
return addcslashes($s, "'\\");
917+
}
918+
919+
private function missValue(string $key): string
920+
{
921+
return $this->context->options->strict
922+
? "LR::miss('" . $this->escape($key) . "')"
923+
: 'null';
924+
}
925+
926+
/** @return array{list<string>, string} [$bp, $bs] */
927+
private function getProgramBlockParams(?Program $program): array
928+
{
929+
$bp = $program ? $program->blockParams : [];
930+
$bs = $bp ? Expression::listString($bp) : 'null';
931+
return [$bp, $bs];
932+
}
933+
934+
private function compileProgramOrEmpty(?Program $program, bool $withSp = false): string
935+
{
936+
return $program ? $this->compileProgram($program, $withSp) : "''";
937+
}
938+
939+
/**
940+
* Return only the string parts of a mixed parts array, re-indexed.
941+
* @param list<string|SubExpression> $parts
942+
* @return list<string>
943+
*/
944+
private static function stringPartsOf(array $parts): array
945+
{
946+
return array_values(array_filter($parts, fn($p) => is_string($p)));
947+
}
948+
926949
/**
927950
* Get includeZero value from hash.
928951
*/
@@ -977,14 +1000,12 @@ private function compilePathWithLookup(PathExpression $path, string $lookupCode)
9771000
{
9781001
$data = $path->data;
9791002
$depth = $path->depth;
980-
$parts = array_values(array_filter($path->parts, fn($p) => is_string($p)));
1003+
$parts = self::stringPartsOf($path->parts);
9811004

9821005
$base = $this->buildBasePath($data, $depth);
9831006
$n = $this->buildKeyAccess($parts);
9841007

985-
$miss = $this->context->options->strict
986-
? "LR::miss('" . addcslashes($path->original, "'\\") . "')"
987-
: 'null';
1008+
$miss = $this->missValue($path->original);
9881009

9891010
return $base . $n . "[$lookupCode] ?? $miss";
9901011
}
@@ -1006,7 +1027,7 @@ private function getWithLookup(AstExpression $itemsExpr, AstExpression $idxExpr)
10061027
$varCode = $this->compilePathWithLookup($itemsExpr, $idxCode);
10071028
} else {
10081029
$itemsCode = $this->compileExpression($itemsExpr);
1009-
$miss = $this->context->options->strict ? "LR::miss('lookup')" : 'null';
1030+
$miss = $this->missValue('lookup');
10101031
$varCode = $itemsCode . "[$idxCode] ?? $miss";
10111032
}
10121033
return $varCode;

src/Runtime.php

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public static function raw(array|string|int|bool|null $v, int $ex = 0): string|a
110110
}
111111

112112
if (is_array($v)) {
113-
if (count(array_diff_key($v, array_keys(array_keys($v)))) > 0) {
113+
if (static::isObjectArray($v)) {
114114
return '[object Object]';
115115
} else {
116116
$ret = '';
@@ -152,16 +152,16 @@ public static function sec(RuntimeContext $cx, mixed $v, ?array $bp, mixed $in,
152152
// #var, detect input type is object or not
153153
if (!$loop && $isAry) {
154154
$keys = array_keys($v);
155-
$loop = (count(array_diff_key($v, array_keys($keys))) == 0);
156-
$isObj = !$loop;
155+
$isObj = static::isObjectArray($v);
156+
$loop = !$isObj;
157157
}
158158

159159
if (($loop && $isAry) || $isTrav) {
160160
if ($each && !$isTrav) {
161161
// Detect input type is object or not when never done once
162162
if ($keys == null) {
163163
$keys = array_keys($v);
164-
$isObj = (count(array_diff_key($v, array_keys($keys))) > 0);
164+
$isObj = static::isObjectArray($v);
165165
}
166166
}
167167
$ret = [];
@@ -274,9 +274,7 @@ public static function wi(RuntimeContext $cx, mixed $v, ?array $bp, array|\stdCl
274274
if ($v === $in) {
275275
$ret = $cb($cx, $v);
276276
} else {
277-
$cx->scopes[] = $in;
278-
$ret = $cb($cx, $v);
279-
array_pop($cx->scopes);
277+
$ret = static::withScope($cx, $in, $v, $cb);
280278
}
281279

282280
$cx->partials = $savedPartials;
@@ -496,9 +494,7 @@ private static function makeBlockFn(RuntimeContext $cx, mixed $_this, ?\Closure
496494
if ($context === null || $context === $_this) {
497495
$ret = $cb($cx, $_this);
498496
} else {
499-
$cx->scopes[] = $_this;
500-
$ret = $cb($cx, $context);
501-
array_pop($cx->scopes);
497+
$ret = static::withScope($cx, $_this, $context, $cb);
502498
}
503499

504500
if (isset($data['data'])) {
@@ -518,10 +514,7 @@ private static function makeInverseFn(RuntimeContext $cx, mixed $_this, ?\Closur
518514
if ($context === null) {
519515
return $else($cx, $_this);
520516
}
521-
$cx->scopes[] = $_this;
522-
$ret = $else($cx, $context);
523-
array_pop($cx->scopes);
524-
return $ret;
517+
return static::withScope($cx, $_this, $context, $else);
525518
}
526519
: fn() => '';
527520
}
@@ -540,6 +533,22 @@ private static function applyBlockHelperMissing(RuntimeContext $cx, mixed $resul
540533
return static::sec($cx, $result, null, $_this, false, $cb ?? static fn() => '', $else);
541534
}
542535

536+
private static function withScope(RuntimeContext $cx, mixed $scope, mixed $context, \Closure $cb): string
537+
{
538+
$cx->scopes[] = $scope;
539+
$ret = $cb($cx, $context);
540+
array_pop($cx->scopes);
541+
return $ret;
542+
}
543+
544+
/**
545+
* @param array<mixed> $v
546+
*/
547+
private static function isObjectArray(array $v): bool
548+
{
549+
return count(array_diff_key($v, array_keys(array_keys($v)))) !== 0;
550+
}
551+
543552
/**
544553
* Execute custom helper with prepared options
545554
*

0 commit comments

Comments
 (0)