XmlRenderer::tag()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 4
nop 3
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace League\CommonMark\Xml;
6
7
use League\CommonMark\Environment\EnvironmentInterface;
8
use League\CommonMark\Event\DocumentPreRenderEvent;
9
use League\CommonMark\Exception\InvalidArgumentException;
10
use League\CommonMark\Node\Block\Document;
11
use League\CommonMark\Node\Node;
12
use League\CommonMark\Node\StringContainerInterface;
13
use League\CommonMark\Output\RenderedContent;
14
use League\CommonMark\Output\RenderedContentInterface;
15
use League\CommonMark\Renderer\DocumentRendererInterface;
16
use League\CommonMark\Util\Xml;
17
18
final class XmlRenderer implements DocumentRendererInterface
19
{
20
    private const INDENTATION = '    ';
21
22
    private EnvironmentInterface $environment;
23
24
    private XmlNodeRendererInterface $fallbackRenderer;
25
26
    /** @var array<class-string, XmlNodeRendererInterface> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, XmlNodeRendererInterface> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, XmlNodeRendererInterface>.
Loading history...
27
    private array $rendererCache = [];
28
29 98
    public function __construct(EnvironmentInterface $environment)
30
    {
31 98
        $this->environment      = $environment;
32 98
        $this->fallbackRenderer = new FallbackNodeXmlRenderer();
33
    }
34
35 98
    public function renderDocument(Document $document): RenderedContentInterface
36
    {
37 98
        $this->environment->dispatch(new DocumentPreRenderEvent($document, 'xml'));
38
39 98
        $xml = '<?xml version="1.0" encoding="UTF-8"?>';
40
41 98
        $indent = 0;
42 98
        $walker = $document->walker();
43 98
        while ($event = $walker->next()) {
44 98
            $node = $event->getNode();
45
46 98
            $closeImmediately = ! $node->hasChildren();
47 98
            $selfClosing      = $closeImmediately && ! $node instanceof StringContainerInterface;
48
49 98
            $renderer = $this->findXmlRenderer($node);
50 98
            $tagName  = $renderer->getXmlTagName($node);
51
52 98
            if ($event->isEntering()) {
53 98
                $attrs = $renderer->getXmlAttributes($node);
54
55 98
                $xml .= "\n" . \str_repeat(self::INDENTATION, $indent);
56 98
                $xml .= self::tag($tagName, $attrs, $selfClosing);
57
58 98
                if ($node instanceof StringContainerInterface) {
59 96
                    $xml .= Xml::escape($node->getLiteral());
60
                }
61
62 98
                if ($closeImmediately && ! $selfClosing) {
63 96
                    $xml .= self::tag('/' . $tagName);
64
                }
65
66 98
                if (! $closeImmediately) {
67 98
                    $indent++;
68
                }
69 98
            } elseif (! $closeImmediately) {
70 98
                $indent--;
71 98
                $xml .= "\n" . \str_repeat(self::INDENTATION, $indent);
72 98
                $xml .= self::tag('/' . $tagName);
73
            }
74
        }
75
76 98
        return new RenderedContent($document, $xml . "\n");
77
    }
78
79
    /**
80
     * @param array<string, string|int|float|bool> $attrs
81
     */
82 98
    private static function tag(string $name, array $attrs = [], bool $selfClosing = \false): string
83
    {
84 98
        $result = '<' . $name;
85 98
        foreach ($attrs as $key => $value) {
86 98
            $result .= \sprintf(' %s="%s"', $key, self::convertAndEscape($value));
87
        }
88
89 98
        if ($selfClosing) {
90 62
            $result .= ' /';
91
        }
92
93 98
        $result .= '>';
94
95 98
        return $result;
96
    }
97
98
    /**
99
     * @param string|int|float|bool $value
100
     */
101 98
    private static function convertAndEscape($value): string
102
    {
103 98
        if (\is_string($value)) {
104 98
            return Xml::escape($value);
105
        }
106
107 44
        if (\is_int($value) || \is_float($value)) {
108 44
            return (string) $value;
109
        }
110
111 6
        if (\is_bool($value)) {
0 ignored issues
show
introduced by
The condition is_bool($value) is always true.
Loading history...
112 6
            return $value ? 'true' : 'false';
113
        }
114
115
        // @phpstan-ignore-next-line
116
        throw new InvalidArgumentException('$value must be a string, int, float, or bool');
117
    }
118
119 98
    private function findXmlRenderer(Node $node): XmlNodeRendererInterface
120
    {
121 98
        $class = \get_class($node);
122
123 98
        if (\array_key_exists($class, $this->rendererCache)) {
124 98
            return $this->rendererCache[$class];
125
        }
126
127 98
        foreach ($this->environment->getRenderersForClass($class) as $renderer) {
128 98
            if ($renderer instanceof XmlNodeRendererInterface) {
129 98
                return $this->rendererCache[$class] = $renderer;
130
            }
131
        }
132
133
        return $this->rendererCache[$class] = $this->fallbackRenderer;
134
    }
135
}
136