Skip to content

Commit b6e4ff1

Browse files
authored
Merge pull request #61 from tronsha/feature/named-slots
Add named slots support
2 parents 81e4241 + 9eaa6c6 commit b6e4ff1

12 files changed

+239
-73
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Compile vue files to twig templates with PHP
2929

3030
|Functionality|Implemented|
3131
|:------------|:---------:|
32-
|Slots||
32+
|Slots|partially working|
3333
|Components|:white_check_mark:|
3434
|Filters||
3535

src/Compiler.php

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Paneon\VueToTwig\Models\Property;
1616
use Paneon\VueToTwig\Models\Replacements;
1717
use Paneon\VueToTwig\Models\Slot;
18+
use Paneon\VueToTwig\Utils\NodeHelper;
1819
use Paneon\VueToTwig\Utils\TwigBuilder;
1920
use Psr\Log\LoggerInterface;
2021
use ReflectionException;
@@ -52,6 +53,11 @@ class Compiler
5253
*/
5354
protected $builder;
5455

56+
/**
57+
* @var NodeHelper
58+
*/
59+
protected $nodeHelper;
60+
5561
/**
5662
* @var Property[]
5763
*/
@@ -88,6 +94,7 @@ class Compiler
8894
public function __construct(DOMDocument $document, LoggerInterface $logger)
8995
{
9096
$this->builder = new TwigBuilder();
97+
$this->nodeHelper = new NodeHelper();
9198
$this->document = $document;
9299
$this->logger = $logger;
93100
$this->lastCloseIf = [];
@@ -183,14 +190,16 @@ public function convertNode(DOMNode $node, int $level = 0): DOMNode
183190
} elseif ($node instanceof DOMDocument) {
184191
$this->logger->warning('Document node found.');
185192
} elseif ($node instanceof DOMElement) {
186-
$this->twigRemove($node);
193+
if ($this->twigRemove($node)) {
194+
return $node;
195+
}
187196
$this->replaceShowWithIf($node);
188197
$this->handleIf($node, $level);
189198
$this->handleFor($node);
190199
$this->handleHtml($node);
191200
$this->handleText($node);
192201
$this->stripEventHandlers($node);
193-
$this->handleDefaultSlot($node);
202+
$this->handleSlots($node);
194203
$this->cleanupAttributes($node);
195204
}
196205

@@ -223,21 +232,13 @@ public function convertNode(DOMNode $node, int $level = 0): DOMNode
223232
$this->convertNode($childNode, $level + 1);
224233
}
225234

226-
// Slots (Default)
235+
// Slots
227236
if ($node->hasChildNodes()) {
228-
$innerHtml = $this->innerHtmlOfNode($node);
229-
$innerHtml = $this->replacePlaceholders($innerHtml);
230-
$this->logger->debug(
231-
'Add default slot:',
232-
[
233-
'nodeValue' => $node->nodeValue,
234-
'innerHtml' => $innerHtml,
235-
]
236-
);
237-
238-
$slot = $usedComponent->addDefaultSlot($innerHtml);
239-
240-
$this->addReplaceVariable($slot->getSlotContentVariableString(), $slot->getValue());
237+
$this->handleNamedSlotsInclude($node, $usedComponent);
238+
// Slots (Default)
239+
if ($node->hasChildNodes() && !$usedComponent->hasSlot(Slot::SLOT_DEFAULT_NAME)) {
240+
$this->addSlot(Slot::SLOT_DEFAULT_NAME, $node, $usedComponent);
241+
}
241242
}
242243

243244
// Include Partial
@@ -271,7 +272,7 @@ public function convertNode(DOMNode $node, int $level = 0): DOMNode
271272
}
272273

273274
// Remove original node
274-
$node->parentNode->removeChild($node);
275+
$this->nodeHelper->removeNode($node);
275276

276277
return $node;
277278
}
@@ -479,7 +480,7 @@ public function handleBinding(string $value, string $name, ?DOMElement $node = n
479480

480481
foreach ($items as $item) {
481482
if (preg_match($regexObjectElements, $item, $matchElement)) {
482-
$dynamicValues[] = $this->prepareBindingOutput(
483+
$dynamicValues[] = $this->builder->prepareBindingOutput(
483484
$this->builder->refactorCondition($matchElement['condition']) . ' ? \'' . $matchElement['class'] . ' \'',
484485
$twigOutput
485486
);
@@ -493,7 +494,7 @@ public function handleBinding(string $value, string $name, ?DOMElement $node = n
493494
foreach ($matches as $match) {
494495
$templateStringContent = str_replace(
495496
$match[0],
496-
$this->prepareBindingOutput($this->builder->refactorCondition($match[1]), $twigOutput),
497+
$this->builder->prepareBindingOutput($this->builder->refactorCondition($match[1]), $twigOutput),
497498
$templateStringContent
498499
);
499500
}
@@ -502,25 +503,12 @@ public function handleBinding(string $value, string $name, ?DOMElement $node = n
502503
} else {
503504
$value = $this->builder->refactorCondition($value);
504505
$this->logger->debug(sprintf('- setAttribute "%s" with value "%s"', $name, $value));
505-
$dynamicValues[] = $this->prepareBindingOutput($value, $twigOutput);
506+
$dynamicValues[] = $this->builder->prepareBindingOutput($value, $twigOutput);
506507
}
507508

508509
return $dynamicValues;
509510
}
510511

511-
private function prepareBindingOutput(string $value, bool $twigOutput = true): string
512-
{
513-
$open = Replacements::getSanitizedConstant('DOUBLE_CURLY_OPEN');
514-
$close = Replacements::getSanitizedConstant('DOUBLE_CURLY_CLOSE');
515-
516-
if (!$twigOutput) {
517-
$open = '(';
518-
$close = ')';
519-
}
520-
521-
return $open . ' ' . $value . ' ' . $close;
522-
}
523-
524512
/**
525513
* @throws ReflectionException
526514
*/
@@ -539,7 +527,7 @@ private function cleanupAttributes(DOMElement $node): void
539527
/** @var DOMAttr $attribute */
540528
foreach ($node->attributes as $attribute) {
541529
if (
542-
(preg_match('/^v-([a-z]*)/', $attribute->name, $matches) === 1 && $matches[1] !== 'bind')
530+
(preg_match('/^v-([a-z]*)/', $attribute->name, $matches) === 1 && $matches[1] !== 'bind' && $matches[1] !== 'slot')
543531
|| preg_match('/^[:]?ref$/', $attribute->name) === 1
544532
) {
545533
$removeAttributes[] = $attribute->name;
@@ -692,25 +680,6 @@ private function handleText(DOMElement $node): void
692680
$node->appendChild(new DOMText('{{' . $text . '}}'));
693681
}
694682

695-
protected function addDefaultsToVariable(string $varName, string $string): string
696-
{
697-
if (!in_array($varName, array_keys($this->properties))) {
698-
return $string;
699-
}
700-
701-
$prop = $this->properties[$varName];
702-
703-
if ($prop->hasDefault()) {
704-
$string = preg_replace(
705-
'/\b(' . $varName . ')\b/',
706-
$varName . '|default(' . $prop->getDefault() . ')',
707-
$string
708-
);
709-
}
710-
711-
return $string;
712-
}
713-
714683
/**
715684
* @throws RuntimeException
716685
*/
@@ -907,28 +876,68 @@ protected function addVariableBlocks(string $string): string
907876
/**
908877
* @throws Exception
909878
*/
910-
protected function handleDefaultSlot(DOMElement $node): void
879+
protected function handleSlots(DOMElement $node): void
911880
{
912881
if ($node->nodeName !== 'slot') {
913882
return;
914883
}
915884

916885
$slotFallback = $node->hasChildNodes() ? $this->innerHtmlOfNode($node) : null;
917886

887+
$slotName = Slot::SLOT_PREFIX;
888+
$slotName .= $node->getAttribute('name') ? $node->getAttribute('name') : Slot::SLOT_DEFAULT_NAME;
889+
918890
if ($slotFallback) {
919-
$this->addVariable('slot_default_fallback', $slotFallback);
920-
$variable = $this->builder->createVariableOutput(
921-
Slot::SLOT_PREFIX . Slot::SLOT_DEFAULT_NAME,
922-
'slot_default_fallback'
923-
);
891+
$this->addVariable($slotName . '_fallback', $slotFallback);
892+
$variable = $this->builder->createVariableOutput($slotName, $slotName . '_fallback');
924893
} else {
925-
$variable = $this->builder->createVariableOutput(Slot::SLOT_PREFIX . Slot::SLOT_DEFAULT_NAME);
894+
$variable = $this->builder->createVariableOutput($slotName);
926895
}
927896

928897
$variableNode = $this->document->createTextNode($variable);
929898

930899
$node->parentNode->insertBefore($variableNode, $node);
931-
$node->parentNode->removeChild($node);
900+
$this->nodeHelper->removeNode($node);
901+
}
902+
903+
/**
904+
* @throws Exception
905+
* @throws ReflectionException
906+
*/
907+
protected function handleNamedSlotsInclude(DOMNode $node, Component $usedComponent): void
908+
{
909+
$removeNodes = [];
910+
foreach ($node->childNodes as $childNode) {
911+
if ($childNode instanceof DOMElement && $childNode->tagName === 'template') {
912+
foreach ($childNode->attributes as $attribute) {
913+
if ($attribute instanceof DOMAttr && preg_match('/v-slot(?::([a-z]+)?)/i', $attribute->nodeName, $matches)) {
914+
$slotName = $matches[1] ?? Slot::SLOT_DEFAULT_NAME;
915+
$this->addSlot($slotName, $childNode, $usedComponent);
916+
$removeNodes[] = $childNode;
917+
}
918+
}
919+
}
920+
}
921+
$this->nodeHelper->removeNodes($removeNodes);
922+
}
923+
924+
/**
925+
* @throws Exception
926+
* @throws ReflectionException
927+
*/
928+
protected function addSlot(string $slotName, DOMNode $node, Component $usedComponent): void
929+
{
930+
$innerHtml = $this->replacePlaceholders($this->innerHtmlOfNode($node));
931+
$this->logger->debug(
932+
'Add ' . $slotName . ' slot:',
933+
[
934+
'nodeValue' => $node->nodeValue,
935+
'innerHtml' => $innerHtml,
936+
]
937+
);
938+
939+
$slot = $usedComponent->addSlot($slotName, $innerHtml);
940+
$this->addReplaceVariable($slot->getSlotContentVariableString(), $slot->getValue());
932941
}
933942

934943
protected function insertDefaultValues(): void
@@ -949,7 +958,7 @@ protected function handleRootNodeAttribute(DOMElement $node, ?string $name = nul
949958
if (!$name) {
950959
return $node;
951960
}
952-
$string = $this->prepareBindingOutput($name . '|default(\'\')');
961+
$string = $this->builder->prepareBindingOutput($name . '|default(\'\')');
953962
if ($node->hasAttribute($name)) {
954963
$attribute = $node->getAttributeNode($name);
955964
$attribute->value .= ' ' . $string;
@@ -965,14 +974,18 @@ private function handleCommentNode(DOMComment $node): void
965974
{
966975
$nodeValue = trim($node->nodeValue);
967976
if (preg_match('/^(eslint-disable|@?todo)/i', $nodeValue) === 1) {
968-
$node->parentNode->removeChild($node);
977+
$this->nodeHelper->removeNode($node);
969978
}
970979
}
971980

972-
private function twigRemove(DOMElement $node): void
981+
private function twigRemove(DOMElement $node): bool
973982
{
974983
if ($node->hasAttribute('data-twig-remove')) {
975984
$node->parentNode->removeChild($node);
985+
986+
return true;
976987
}
988+
989+
return false;
977990
}
978991
}

src/Models/Component.php

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,6 @@ public function addProperty(string $name, string $value, bool $isBinding = false
7474
);
7575
}
7676

77-
/**
78-
* @throws Exception
79-
*/
80-
public function addDefaultSlot(string $value): Slot
81-
{
82-
return $this->addSlot(Slot::SLOT_DEFAULT_NAME, $value);
83-
}
84-
8577
/**
8678
* @throws Exception
8779
*/
@@ -114,6 +106,11 @@ public function hasSlots(): bool
114106
return !empty($this->slots);
115107
}
116108

109+
public function hasSlot(string $name): bool
110+
{
111+
return !empty($this->slots[$name]);
112+
}
113+
117114
/**
118115
* @return Slot[]
119116
*/

src/Utils/NodeHelper.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Paneon\VueToTwig\Utils;
6+
7+
use DOMNode;
8+
9+
class NodeHelper
10+
{
11+
/**
12+
* @param DOMNode[] $nodes
13+
*/
14+
public function removeNodes(array $nodes): void
15+
{
16+
foreach ($nodes as $node) {
17+
$this->removeNode($node);
18+
}
19+
}
20+
21+
public function removeNode(DOMNode $node): void
22+
{
23+
$node->parentNode->removeChild($node);
24+
}
25+
}

src/Utils/TwigBuilder.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,17 @@ public function createVariableOutput(string $varName, ?string $fallbackVariableN
327327

328328
return '{{ ' . $varName . ' }}';
329329
}
330+
331+
public function prepareBindingOutput(string $value, bool $twigOutput = true): string
332+
{
333+
$open = Replacements::getSanitizedConstant('DOUBLE_CURLY_OPEN');
334+
$close = Replacements::getSanitizedConstant('DOUBLE_CURLY_CLOSE');
335+
336+
if (!$twigOutput) {
337+
$open = '(';
338+
$close = ')';
339+
}
340+
341+
return $open . ' ' . $value . ' ' . $close;
342+
}
330343
}

tests/DataTwigRemoveTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,20 @@ public function testDataTwigRemove()
2121

2222
$this->assertEqualHtml($expected, $actual);
2323
}
24+
25+
/**
26+
* @throws Exception
27+
*/
28+
public function testDataTwigRemoveWithIf()
29+
{
30+
$vueTemplate = '<template><div><span v-if="true" data-twig-remove>dummy</span></div></template>';
31+
32+
$expected = '<div class="{{ class|default(\'\') }}" style="{{ style|default(\'\') }}"></div>';
33+
34+
$compiler = $this->createCompiler($vueTemplate);
35+
36+
$actual = $compiler->convert();
37+
38+
$this->assertEqualHtml($expected, $actual);
39+
}
2440
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% set slot_header_fallback %}header{% endset %}
2+
{% set slot_default_fallback %}content{% endset %}
3+
{% set slot_footer_fallback %}footer{% endset %}
4+
<div class="container {{ class|default('') }}" style="{{ style|default('') }}">
5+
<header>{{ slot_header|default(slot_header_fallback) }}</header>
6+
<main>{{ slot_default|default(slot_default_fallback) }}</main>
7+
<footer>{{ slot_footer|default(slot_footer_fallback) }}</footer>
8+
</div>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<template>
2+
<div class="container">
3+
<header>
4+
<slot name="header">header</slot>
5+
</header>
6+
<main>
7+
<slot>content</slot>
8+
</main>
9+
<footer>
10+
<slot name="footer">footer</slot>
11+
</footer>
12+
</div>
13+
</template>
14+
<script>
15+
export default {
16+
name: 'BaseLayout',
17+
};
18+
</script>

0 commit comments

Comments
 (0)