Completed
Push — master ( c7a15d...dd0bc7 )
by Vitaly
03:37
created

Tree   A

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 17
Bugs 1 Features 4
Metric Value
wmc 33
c 17
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
namespace samsonframework\html2less;
3
4
/**
5
 * Created by Vitaly Iegorov <[email protected]>.
6
 * on 15.04.16 at 12:43
7
 */
8
class Tree
9
{
10
    /** @var array Collection of ignored DOM nodes */
11
    public static $ignoredNodes = array(
12
        'head',
13
        'meta',
14
        'script',
15
        'noscript',
16
        'link',
17
        'title',
18
        'br',
19
    );
20
21
    /**
22
     * Build destination code tree from source code.
23
     *
24
     * @param string $source Source code
25
     *
26
     * @return Node Less tree root node
27
     */
28 2
    public function build($source)
29
    {
30
        // Prepare source code
31 2
        $source = $this->prepare($source);
32
33
        // Build destination node tree
34 2
        return $this->analyze($source);
35
    }
36
37
    /**
38
     * Source code cleaner.
39
     *
40
     * @param string $source
41
     *
42
     * @return string Cleared source code
43
     */
44 2
    protected function prepare($source)
45
    {
46
        // Remove all PHP code from view
47 2
        return trim(preg_replace('/<\?(php|=).*?\?>/', '', $source));
48
    }
49
50
    /**
51
     * Analyze source code and create destination code tree.
52
     *
53
     * @param string $source Source code
54
     *
55
     * @return Node Internal code tree
56
     */
57 2
    protected function &analyze($source)
58
    {
59 2
        libxml_use_internal_errors(true);
60
61
        /** @var \DOMDocument $dom Pointer to current dom element */
62 2
        $dom = new \DOMDocument();
63 2
        $dom->loadHTML($source, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
64
65
        // Perform recursive node analysis
66 2
        return $this->analyzeSourceNode($dom, new Node('', ''));
67
    }
68
69
    /**
70
     * Perform source node analysis.
71
     *
72
     * @param \DOMNode $domNode
73
     * @param Node     $parent
74
     *
75
     * @return Node
76
     */
77 2
    protected function &analyzeSourceNode(\DOMNode $domNode, Node $parent)
78
    {
79
        /** @var Node[string] $tagNodes Group current level nodes by tags */
0 ignored issues
show
Documentation introduced by
The doc-type Node[string] could not be parsed: Expected "]" at position 2, but found "string". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
80 2
        $tagNodes = [];
81
82 2
        foreach ($domNode->childNodes as $child) {
83 2
            $tag = $child->nodeName;
84
85
            // Work only with allowed DOMElements
86 2
            if ($child->nodeType === 1 && !in_array($tag, static::$ignoredNodes, true)) {
87
                // Get node classes
88 2
                $classes = array_filter(explode(' ', $this->getDOMAttributeValue($child, 'class')));
89
90
                // Get LESS node selector
91 2
                $selector = $this->getSelector($child, $tag, $classes);
92
93
                // Ignore divs as generic markup element
94 2
                if ($selector !== 'div') {
95
                    // Find child node by selector
96 2
                    $node = $parent !== null ? $parent->getChild($selector) : null;
97
98
                    // Check if we have created this selector LessNode for this branch
99 2
                    if (null === $node) {
100
                        // Create internal node instance
101 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...
102 2
                    }
103
104
                    // Group LESS nodes by tag name
105 2
                    if ($tag !== 'div') {
106 2
                        $tagNodes[$tag][$selector] = $node;
107 2
                    }
108
109
                    // Create inner class modifiers for parent node
110 2
                    foreach ($classes as $class) {
111
                        // Create inner node without correct tag to avoid grouping bugs
112 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...
113 2
                    }
114
115
                    // Go deeper in recursion
116 2
                    $this->analyzeSourceNode($child, $node);
117 2
                } else {
118
                    // Go deeper in recursion
119 1
                    $this->analyzeSourceNode($child, $parent);
120
                }
121 2
            }
122 2
        }
123
124 2
        if (null !== $parent) {
125 2
            $this->optimizeGroupTags($tagNodes, $parent);
126 2
        }
127
128 2
        return $parent;
129
    }
130
131
    /**
132
     * Get DOM node attribute value.
133
     *
134
     * @param \DOMNode $domNode
135
     * @param string   $attributeName
136
     *
137
     * @return null|string DOM node attribute value
138
     */
139 2
    protected function getDOMAttributeValue(\DOMNode $domNode, $attributeName)
140
    {
141 2
        if (null !== $domNode->attributes) {
142
            /**@var \DOMAttr $attribute */
143 2
            foreach ($domNode->attributes as $attribute) {
144 2
                $value = trim($attribute->nodeValue);
145
                // If DOM attribute matches needed
146 2
                if ($attributeName === $attribute->name && $value !== '') {
147
                    // Remove white spaces
148 2
                    return $value;
149
                }
150 2
            }
151 2
        }
152
153 2
        return null;
154
    }
155
156
    /**
157
     * Get current \DOMNode LESS selector.
158
     *
159
     * @param \DOMNode $child
160
     * @param string   $tag
161
     * @param array    $classes
162
     *
163
     * @return string LESS selector
164
     */
165 2
    protected function getSelector(\DOMNode $child, $tag, array &$classes)
166
    {
167
        // Define less node selector
168 2
        if (($identifier = $this->getDOMAttributeValue($child, 'id')) !== null) {
169 1
            return '#' . $identifier;
170 2
        } elseif (count($classes)) {
171 2
            return '.' . array_shift($classes);
172 1
        } elseif (($name = $this->getDOMAttributeValue($child, 'name')) !== null) {
173
            return $tag . '[name=' . $name . ']';
174
        } else {
175 1
            return $tag;
176
        }
177
    }
178
179
    /**
180
     * Optimize by grouping tag name LESS nodes.
181
     *
182
     * @param      Node [string] $tagNodes
183
     * @param Node $parent
184
     */
185 2
    protected function optimizeGroupTags($tagNodes, Node $parent)
186
    {
187
        // Check if we have inner nodes with same tag
188 2
        foreach ($tagNodes as $tag => $nodes) {
189 2
            if (count($nodes) > 1) {
190
                /**
191
                 * @var Node $matchingTagNode
192
                 * If we already had LESS node for this tag then we have
193
                 * already replaced it with group tag so we do not need
194
                 * to re-remove it from parent as it is already a new one
195
                 */
196 2
                $matchingTagNode = null;
197 2
                if (array_key_exists($tag, $nodes)) {
198 1
                    $matchingTagNode = $nodes[$tag];
199 1
                    unset($nodes[$tag]);
200 1
                }
201
202 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...
203
204 2
                foreach ($nodes as $selector => $child) {
205
                    // Attach child to new grouped tag node
206 2
                    $child->parent = &$tagNode;
207 2
                    $tagNode->children[$selector] = $child;
208
                    // Append & for inner nodes
209 2
                    $child->selector = '&' . ltrim($child->selector, '&');
210
                    // Remove child from current parent
211 2
                    unset($parent->children[$selector]);
212 2
                }
213
214
                // Add matching tag node children to new grouped tag
215 2
                if (null !== $matchingTagNode) {
216 1
                    foreach ($matchingTagNode->children as $selector => $child) {
217
                        // Attach child to new grouped tag node
218 1
                        $child->parent = &$tagNode;
219 1
                        $tagNode->children[$selector] = $child;
220 1
                    }
221 1
                }
222 2
            }
223 2
        }
224 2
    }
225
226
    /**
227
     * Render LESS tree. This function is recursive.
228
     *
229
     * @param Node   $node   Current LESS tree node
230
     * @param string $output Final LESS code string
231
     * @param int    $level  Current recursion level
232
     *
233
     * @return string LESS code
234
     */
235 2
    public function output(Node $node, $output = '', $level = 0)
236
    {
237
        // Output less node with spaces
238 2
        $output .= $this->spaces($level) . (isset($node->selector{0}) ? $node . ' ' : '') . '{' . "\n";
239
240 2
        foreach ($node->children as $child) {
0 ignored issues
show
Bug introduced by
The expression $node->children of type object<samsonframework\html2less\Node> is not traversable.
Loading history...
241 2
            $output = $this->output($child, $output, $level + 1);
242 2
        }
243
244
        // Close less node with spaces
245 2
        $output .= $this->spaces($level) . '}' . "\n";
246
247 2
        return $output;
248
    }
249
250
    /**
251
     * Get spaces for LESS tree level.
252
     *
253
     * @param int $level LESS tree depth
254
     *
255
     * @return string Spaces for current LESS tree depth
256
     */
257 2
    protected function spaces($level = 0)
258
    {
259 2
        return implode('', array_fill(0, $level, '  '));
260
    }
261
}
262