Passed
Pull Request — master (#993)
by Maxim
20:44 queued 10:42
created

FormatHTML::leaveNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Stempler\Visitor;
6
7
use Spiral\Stempler\Node\Block;
8
use Spiral\Stempler\Node\HTML\Attr;
9
use Spiral\Stempler\Node\HTML\Tag;
10
use Spiral\Stempler\Node\PHP;
11
use Spiral\Stempler\Node\Raw;
12
use Spiral\Stempler\Node\Template;
13
use Spiral\Stempler\VisitorContext;
14
use Spiral\Stempler\VisitorInterface;
15
16
/**
17
 * Set proper indents for all HTML tags.
18
 */
19
final class FormatHTML implements VisitorInterface
20
{
21
    // default indent
22
    private const INDENT = '  ';
23
24
    private const EXCLUDE = ['pre', 'textarea'];
25
26
    // indent exceptions
27
    private const BETWEEN_TAGS = 0;
28
    private const BEFORE_PHP   = 1;
29
    private const BEFORE_CLOSE = 2;
30
31 14
    public function enterNode(mixed $node, VisitorContext $ctx): mixed
32
    {
33 14
        if (!$node instanceof Template && !$node instanceof Block && !$node instanceof Tag) {
34 14
            return null;
35
        }
36
37 14
        if ($node instanceof Tag && \in_array($node->name, self::EXCLUDE)) {
38
            // raw nodes
39 1
            return null;
40
        }
41
42 14
        $level = $this->getLevel($ctx);
43 14
        if ($level === null) {
44
            // not available in some contexts
45
            return null;
46
        }
47
48 14
        foreach ($node->nodes as $i => $child) {
49 14
            if (!$child instanceof Raw) {
50 11
                continue;
51
            }
52
53 13
            $position = self::BETWEEN_TAGS;
54 13
            if (!isset($node->nodes[$i + 1])) {
55 13
                $position = self::BEFORE_CLOSE;
56 5
            } elseif ($node->nodes[$i + 1] instanceof PHP) {
57 3
                $position = self::BEFORE_PHP;
58
            }
59
60 13
            $child->content = $this->indentContent(
61 13
                $this->normalizeEndings((string)$child->content, false),
62 13
                $level,
63 13
                $position
64 13
            );
65
        }
66
67 14
        return null;
68
    }
69
70 14
    public function leaveNode(mixed $node, VisitorContext $ctx): mixed
71
    {
72 14
        return null;
73
    }
74
75 13
    private function indentContent(string $content, int $level, int $position = self::BETWEEN_TAGS): string
76
    {
77 13
        if (!\str_contains($content, "\n")) {
78
            // no need to do anything
79 3
            return $content;
80
        }
81
82
        // we have to apply special rules to the first and the last lines
83 10
        $lines = \explode("\n", $content);
84
85 10
        foreach ($lines as $i => $line) {
86 10
            if (\trim($line) === '' && $i !== 0) {
87 8
                unset($lines[$i]);
88
            }
89
        }
90
91 10
        $lines = \array_values($lines);
92 10
        if ($lines === []) {
93
            $lines[] = '';
94
        }
95
96 10
        $result = '';
97 10
        foreach ($lines as $i => $line) {
98 10
            if (\trim($line) !== '') {
99 8
                $line = $i === 0 ? \rtrim($line) : \trim($line);
100
            }
101
102 10
            if ($i !== (\count($lines) - 1)) {
103 7
                $result .= $line . "\n" . \str_repeat(self::INDENT, $level);
104 7
                continue;
105
            }
106
107
            // working with last line
108 10
            if ($position === self::BEFORE_PHP) {
109 2
                $result .= $line . "\n";
110 2
                break;
111
            }
112
113 10
            if ($position === self::BEFORE_CLOSE) {
114 10
                $result .= $line . "\n" . \str_repeat(self::INDENT, max($level - 1, 0));
115 10
                break;
116
            }
117
118 3
            $result .= $line . "\n" . \str_repeat(self::INDENT, $level);
119
        }
120
121 10
        return $result;
122
    }
123
124 14
    private function getLevel(VisitorContext $ctx): ?int
125
    {
126 14
        $level = 0;
127 14
        foreach ($ctx->getScope() as $node) {
128 14
            if ($node instanceof Attr) {
129
                return null;
130
            }
131
132 14
            if ($node instanceof Block || $node instanceof Template) {
133 14
                continue;
134
            }
135
136 5
            $level++;
137
        }
138
139 14
        return $level;
140
    }
141
142
    /**
143
     * Normalize string endings to avoid EOL problem. Replace \n\r and multiply new lines with
144
     * single \n.
145
     *
146
     * @param string $string       String to be normalized.
147
     * @param bool   $joinMultiple Join multiple new lines into one.
148
     */
149 13
    private function normalizeEndings(string $string, bool $joinMultiple = true): string
150
    {
151 13
        if (!$joinMultiple) {
152 13
            return \str_replace("\r\n", "\n", $string);
153
        }
154
155
        return \preg_replace('/[\n\r]+/', "\n", $string);
156
    }
157
}
158