Passed
Push — master ( a3ff66...3ad8df )
by Matt
01:45
created

DefaultParser::parseAttributes()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 27
ccs 13
cts 13
cp 1
rs 9.3888
c 0
b 0
f 0
cc 5
nc 2
nop 1
crap 5
1
<?php
2
3
namespace Maiorano\Shortcodes\Parsers;
4
5
use Closure;
6
use Generator;
7
8
/**
9
 * Class DefaultParser.
10
 */
11
class DefaultParser implements ParserInterface
12
{
13
    /**
14
     * @param string       $content
15
     * @param array        $tags
16
     * @param Closure|null $callback
17
     *
18
     * @return array|string|string[]|null
19
     */
20 2
    public function parseShortcode(string $content, array $tags, Closure $callback = null)
21
    {
22 2
        if (strpos($content, '[') === false && empty($tags)) {
23 2
            return is_null($callback) ? [] : $content;
24
        }
25
26 2
        $regex = $this->getRegex($tags);
27
28 2
        preg_match_all("/$regex/", $content, $matches, PREG_SET_ORDER);
29
30 2
        if (is_null($callback)) {
31 1
            return iterator_to_array($this->generateResults($matches));
32
        }
33
34
        return preg_replace_callback("/$regex/", function ($match) use ($callback) {
35 1
            if ($match[1] == '[' && $match[6] == ']') {
36 1
                return substr($match[0], 1, -1);
37
            }
38
39 1
            $content = isset($match[5]) ? $match[5] : null;
40 1
            $atts = isset($match[3]) ? $this->parseAttributes($match[3]) : [];
41
42 1
            return $callback($match[2], $content, $atts);
43 1
        }, $content);
44
    }
45
46
    /**
47
     * @param array $tags
48
     *
49
     * @return string
50
     *
51
     * @see https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/shortcodes.php#L228
52
     */
53 2
    private function getRegex(array $tags): string
54
    {
55 2
        $tagregexp = implode('|', array_map('preg_quote', $tags));
56
57
        return
58
            '\\['                // Opening bracket
59
            .'(\\[?)'           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
60 2
            ."($tagregexp)"     // 2: Shortcode name
61 2
            .'(?![\\w-])'       // Not followed by word character or hyphen
62 2
            .'('                // 3: Unroll the loop: Inside the opening shortcode tag
63 2
            .'[^\\]\\/]*'       // Not a closing bracket or forward slash
64 2
            .'(?:'
65 2
            .'\\/(?!\\])'       // A forward slash not followed by a closing bracket
66 2
            .'[^\\]\\/]*'       // Not a closing bracket or forward slash
67 2
            .')*?'
68 2
            .')'
69 2
            .'(?:'
70 2
            .'(\\/)'            // 4: Self closing tag ...
71 2
            .'\\]'              // ... and closing bracket
72 2
            .'|'
73 2
            .'\\]'              // Closing bracket
74 2
            .'(?:'
75 2
            .'('                // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
76 2
            .'[^\\[]*+'         // Not an opening bracket
77 2
            .'(?:'
78 2
            .'\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag
79 2
            .'[^\\[]*+'         // Not an opening bracket
80 2
            .')*+'
81 2
            .')'
82 2
            .'\\[\\/\\2\\]'     // Closing shortcode tag
83 2
            .')?'
84 2
            .')'
85 2
            .'(\\]?)';          // 6: Optional second closing brocket for escaping shortcodes: [[tag]]
86
    }
87
88
    /**
89
     * @param string $text
90
     *
91
     * @return array
92
     *
93
     * @see https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/shortcodes.php#L482
94
     */
95 9
    public function parseAttributes(string $text): array
96
    {
97 9
        $atts = [];
98 9
        $patterns = implode('|', [
99 9
            '([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)', // attribute="value"
100
            '([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)', // attribute='value'
101
            '([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)', // attribute=value
102
            '"([^"]*)"(?:\s|$)', // "attribute"
103
            '\'([^\']*)\'(?:\s|$)', // 'attribute'
104
            '(\S+)(?:\s|$)', // attribute
105
        ]);
106 9
        $pattern = "/{$patterns}/";
107 9
        $text = preg_replace("/[\x{00a0}\x{200b}]+/u", ' ', $text);
108 9
        if (preg_match_all($pattern, (string) $text, $match, PREG_SET_ORDER)) {
109
110
            // Reject any unclosed HTML elements
111 7
            foreach ($this->generateAttributes($match) as $att => $value) {
112 7
                if (strpos($value, '<') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type true; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
                if (strpos(/** @scrutinizer ignore-type */ $value, '<') !== false) {
Loading history...
113 1
                    if (preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value) !== 1) {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type true; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
                    if (preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', /** @scrutinizer ignore-type */ $value) !== 1) {
Loading history...
114 1
                        $value = '';
115
                    }
116
                }
117 7
                $atts[$att] = $value;
118
            }
119
        }
120
121 9
        return $atts;
122
    }
123
124
    /**
125
     * @param array $matches
126
     *
127
     * @return Generator
128
     */
129 7
    private function generateAttributes(array $matches): Generator
130
    {
131 7
        foreach ($matches as $m) {
132 7
            if (!empty($m[1])) {
133 2
                yield strtolower($m[1]) => stripcslashes($m[2]);
134 5
            } elseif (!empty($m[3])) {
135 1
                yield strtolower($m[3]) => stripcslashes($m[4]);
136 4
            } elseif (!empty($m[5])) {
137 1
                yield strtolower($m[5]) => stripcslashes($m[6]);
138 3
            } elseif (isset($m[7]) && strlen($m[7])) {
139 1
                yield strtolower($m[7]) => true;
140 2
            } elseif (isset($m[8]) && strlen($m[8])) {
141 1
                yield strtolower($m[8]) => true;
142 1
            } elseif (isset($m[9])) {
143 7
                yield strtolower($m[9]) => true;
144
            }
145
        }
146 7
    }
147
148
    /**
149
     * @param array $matches
150
     *
151
     * @return Generator
152
     */
153 1
    private function generateResults(array $matches): Generator
154
    {
155 1
        foreach ($matches as $match) {
156 1
            if ($match[1] == '[' && $match[6] == ']') {
157 1
                continue;
158
            }
159
            yield [
160 1
                'tag'        => $match[2],
161 1
                'content'    => isset($match[5]) ? $match[5] : null,
162 1
                'attributes' => isset($match[3]) ? $this->parseAttributes($match[3]) : [],
163
            ];
164
        }
165 1
    }
166
}
167