Completed
Push — master ( a91f05...ae208f )
by Vitaly
02:11
created

Tree::getSelector()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

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