Tree   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 254
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 98.85%

Importance

Changes 19
Bugs 1 Features 4
Metric Value
wmc 33
c 19
b 1
f 4
lcom 1
cbo 1
dl 0
loc 254
ccs 86
cts 87
cp 0.9885
rs 9.3999

9 Methods

Rating   Name   Duplication   Size   Complexity  
A build() 0 8 1
A prepare() 0 5 1
A analyze() 0 11 1
C analyzeSourceNode() 0 53 10
B getDOMAttributeValue() 0 16 5
A getSelector() 0 13 4
C optimizeGroupTags() 0 40 7
A output() 0 14 3
A spaces() 0 4 1
1
<?php
2
3
namespace samsonframework\html2less;
4
5
/**
6
 * Created by Vitaly Iegorov <[email protected]>.
7
 * on 15.04.16 at 12:43.
8
 */
9
class Tree
10
{
11
    /** @var array Collection of ignored DOM nodes */
12
    public static $ignoredNodes = array(
13
        'head',
14
        'meta',
15
        'script',
16
        'noscript',
17
        'link',
18
        'title',
19
        'br',
20
    );
21
22
    /**
23
     * Build destination code tree from source code.
24
     *
25
     * @param string $source Source code
26
     *
27
     * @return Node Less tree root node
28
     */
29 2
    public function build($source)
30
    {
31
        // Prepare source code
32 2
        $source = $this->prepare($source);
33
34
        // Build destination node tree
35 2
        return $this->analyze($source);
36
    }
37
38
    /**
39
     * Source code cleaner.
40
     *
41
     * @param string $source
42
     *
43
     * @return string Cleared source code
44
     */
45 2
    protected function prepare($source)
46
    {
47
        // Remove all PHP code from view
48 2
        return trim(preg_replace('/<\?(php|=).*?\?>/', '', $source));
49
    }
50
51
    /**
52
     * Analyze source code and create destination code tree.
53
     *
54
     * @param string $source Source code
55
     *
56
     * @return Node Internal code tree
57
     */
58 2
    protected function &analyze($source)
59
    {
60 2
        libxml_use_internal_errors(true);
61
62
        /** @var \DOMDocument $dom Pointer to current dom element */
63 2
        $dom = new \DOMDocument();
64 2
        $dom->loadHTML($source, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
65
66
        // Perform recursive node analysis
67 2
        return $this->analyzeSourceNode($dom, new Node('', ''));
68
    }
69
70
    /**
71
     * Perform source node analysis.
72
     *
73
     * @param \DOMNode $domNode
74
     * @param Node     $parent
75
     *
76
     * @return Node
77
     */
78 2
    protected function &analyzeSourceNode(\DOMNode $domNode, Node $parent)
79
    {
80
        /** @var Node[] $tagNodes Group current level nodes by tags */
81 2
        $tagNodes = [];
82
83 2
        foreach ($domNode->childNodes as $child) {
84 2
            $tag = $child->nodeName;
85
86
            // Work only with allowed DOMElements
87 2
            if ($child->nodeType === 1 && !in_array($tag, static::$ignoredNodes, true)) {
88
                // Get node classes
89 2
                $classes = array_filter(explode(' ', $this->getDOMAttributeValue($child, 'class')));
90
91
                // Get LESS node selector
92 2
                $selector = $this->getSelector($child, $tag, $classes);
93
94
                // Ignore divs as generic markup element
95 2
                if ($selector !== 'div') {
96
                    // Find child node by selector
97 2
                    $node = $parent !== null ? $parent->getChild($selector) : null;
98
99
                    // Check if we have created this selector LessNode for this branch
100 2
                    if (null === $node) {
101
                        // Create internal node instance
102 2
                        $node = new Node($selector, $tag, $parent);
0 ignored issues
show
Documentation introduced by
$parent is of type object<samsonframework\html2less\Node>, but the function expects a null|object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
103 2
                    }
104
105
                    // Group LESS nodes by tag name
106 2
                    if ($tag !== 'div') {
107 2
                        $tagNodes[$tag][$selector] = $node;
108 2
                    }
109
110
                    // Create inner class modifiers for parent node
111 2
                    foreach ($classes as $class) {
112
                        // Create inner node without correct tag to avoid grouping bugs
113 2
                        new Node('&.' . $class, '&.' . $class, $node);
0 ignored issues
show
Documentation introduced by
$node is of type object<samsonframework\html2less\Node>, but the function expects a null|object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
114 2
                    }
115
116
                    // Go deeper in recursion
117 2
                    $this->analyzeSourceNode($child, $node);
118 2
                } else {
119
                    // Go deeper in recursion
120 1
                    $this->analyzeSourceNode($child, $parent);
121
                }
122 2
            }
123 2
        }
124
125 2
        if (null !== $parent) {
126 2
            $this->optimizeGroupTags($tagNodes, $parent);
127 2
        }
128
129 2
        return $parent;
130
    }
131
132
    /**
133
     * Get DOM node attribute value.
134
     *
135
     * @param \DOMNode $domNode
136
     * @param string   $attributeName
137
     *
138
     * @return null|string DOM node attribute value
139
     */
140 2
    protected function getDOMAttributeValue(\DOMNode $domNode, $attributeName)
141
    {
142 2
        if (null !== $domNode->attributes) {
143
            /**@var \DOMAttr $attribute */
144 2
            foreach ($domNode->attributes as $attribute) {
145 2
                $value = trim($attribute->nodeValue);
146
                // If DOM attribute matches needed
147 2
                if ($attributeName === $attribute->name && $value !== '') {
148
                    // Remove white spaces
149 2
                    return $value;
150
                }
151 2
            }
152 2
        }
153
154 2
        return;
155
    }
156
157
    /**
158
     * Get current \DOMNode LESS selector.
159
     *
160
     * @param \DOMNode $child
161
     * @param string   $tag
162
     * @param array    $classes
163
     *
164
     * @return string LESS selector
165
     */
166 2
    protected function getSelector(\DOMNode $child, $tag, array &$classes)
167
    {
168
        // Define less node selector
169 2
        if (($identifier = $this->getDOMAttributeValue($child, 'id')) !== null) {
170 1
            return '#' . $identifier;
171 2
        } elseif (count($classes)) {
172 2
            return '.' . array_shift($classes);
173 1
        } elseif (($name = $this->getDOMAttributeValue($child, 'name')) !== null) {
174
            return $tag . '[name=' . $name . ']';
175
        } else {
176 1
            return $tag;
177
        }
178
    }
179
180
    /**
181
     * Optimize by grouping tag name LESS nodes.
182
     *
183
     * @param Node[] $tagNodes
184
     * @param Node   $parent
185
     */
186 2
    protected function optimizeGroupTags($tagNodes, Node $parent)
187
    {
188
        // Check if we have inner nodes with same tag
189 2
        foreach ($tagNodes as $tag => $nodes) {
190 2
            if (count($nodes) > 1) {
191
                /**
192
                 * @var Node
193
                 *           If we already had LESS node for this tag then we have
194
                 *           already replaced it with group tag so we do not need
195
                 *           to re-remove it from parent as it is already a new one
196
                 */
197 2
                $matchingTagNode = null;
198 2
                if (array_key_exists($tag, $nodes)) {
199 1
                    $matchingTagNode = $nodes[$tag];
200 1
                    unset($nodes[$tag]);
201 1
                }
202
203 2
                $tagNode = new Node($tag, $tag, $parent);
0 ignored issues
show
Documentation introduced by
$parent is of type object<samsonframework\html2less\Node>, but the function expects a null|object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
204
205 2
                foreach ($nodes as $selector => $child) {
0 ignored issues
show
Bug introduced by
The expression $nodes of type object<samsonframework\html2less\Node> is not traversable.
Loading history...
206
                    // Attach child to new grouped tag node
207 2
                    $child->parent = &$tagNode;
208 2
                    $tagNode->children[$selector] = $child;
209
                    // Append & for inner nodes
210 2
                    $child->selector = '&' . ltrim($child->selector, '&');
211
                    // Remove child from current parent
212 2
                    unset($parent->children[$selector]);
213 2
                }
214
215
                // Add matching tag node children to new grouped tag
216 2
                if (null !== $matchingTagNode) {
217 1
                    foreach ($matchingTagNode->children as $selector => $child) {
218
                        // Attach child to new grouped tag node
219 1
                        $child->parent = &$tagNode;
220 1
                        $tagNode->children[$selector] = $child;
221 1
                    }
222 1
                }
223 2
            }
224 2
        }
225 2
    }
226
227
    /**
228
     * Render LESS tree. This function is recursive.
229
     *
230
     * @param Node   $node   Current LESS tree node
231
     * @param string $output Final LESS code string
232
     * @param int    $level  Current recursion level
233
     *
234
     * @return string LESS code
235
     */
236 2
    public function output(Node $node, $output = '', $level = 0)
237
    {
238
        // Output less node with spaces
239 2
        $output .= $this->spaces($level) . (isset($node->selector{0}) ? $node . ' ' : '') . '{' . "\n";
240
241 2
        foreach ($node->children as $child) {
242 2
            $output = $this->output($child, $output, $level + 1);
243 2
        }
244
245
        // Close less node with spaces
246 2
        $output .= $this->spaces($level) . '}' . "\n";
247
248 2
        return $output;
249
    }
250
251
    /**
252
     * Get spaces for LESS tree level.
253
     *
254
     * @param int $level LESS tree depth
255
     *
256
     * @return string Spaces for current LESS tree depth
257
     */
258 2
    protected function spaces($level = 0)
259
    {
260 2
        return implode('', array_fill(0, $level, '  '));
261
    }
262
}
263