DefaultParser   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 155
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 26
eloc 74
c 4
b 1
f 0
dl 0
loc 155
ccs 71
cts 71
cp 1
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getRegex() 0 33 1
A parseShortcode() 0 15 5
A generateResults() 0 10 6
A generateAttributes() 0 8 3
A generateCallback() 0 11 5
A parseAttributes() 0 27 6
1
<?php
2
3
namespace Maiorano\Shortcodes\Parsers;
4
5
use Closure;
6
use Generator;
7
8
/**
9
 * Class DefaultParser.
10
 */
11
final class DefaultParser implements ParserInterface
12
{
13
    /**
14
     * @param string       $content
15
     * @param string[]     $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 1
        return preg_replace_callback("/$regex/", $this->generateCallback($callback), $content);
35
    }
36
37
    /**
38
     * @param string[] $tags
39
     *
40
     * @return string
41
     *
42
     * @see https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/shortcodes.php#L228
43
     */
44 2
    private function getRegex(array $tags): string
45
    {
46 2
        $tagregexp = implode('|', array_map('preg_quote', $tags));
47
48
        return
49
            '\\['                // Opening bracket
50
            .'(\\[?)'           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
51 2
            ."($tagregexp)"     // 2: Shortcode name
52 2
            .'(?![\\w-])'       // Not followed by word character or hyphen
53 2
            .'('                // 3: Unroll the loop: Inside the opening shortcode tag
54 2
            .'[^\\]\\/]*'       // Not a closing bracket or forward slash
55 2
            .'(?:'
56 2
            .'\\/(?!\\])'       // A forward slash not followed by a closing bracket
57 2
            .'[^\\]\\/]*'       // Not a closing bracket or forward slash
58 2
            .')*?'
59 2
            .')'
60 2
            .'(?:'
61 2
            .'(\\/)'            // 4: Self closing tag ...
62 2
            .'\\]'              // ... and closing bracket
63 2
            .'|'
64 2
            .'\\]'              // Closing bracket
65 2
            .'(?:'
66 2
            .'('                // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
67 2
            .'[^\\[]*+'         // Not an opening bracket
68 2
            .'(?:'
69 2
            .'\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag
70 2
            .'[^\\[]*+'         // Not an opening bracket
71 2
            .')*+'
72 2
            .')'
73 2
            .'\\[\\/\\2\\]'     // Closing shortcode tag
74 2
            .')?'
75 2
            .')'
76 2
            .'(\\]?)';          // 6: Optional second closing brocket for escaping shortcodes: [[tag]]
77
    }
78
79
    /**
80
     * @param string $text
81
     *
82
     * @return mixed[]
83
     *
84
     * @see https://core.trac.wordpress.org/browser/tags/4.9/src/wp-includes/shortcodes.php#L482
85
     */
86 9
    public function parseAttributes(string $text): array
87
    {
88 9
        $atts = [];
89 9
        $patterns = implode('|', [
90 9
            '([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)', // attribute="value"
91
            '([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)', // attribute='value'
92
            '([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)', // attribute=value
93
            '"([^"]*)"(?:\s|$)', // "attribute"
94
            '\'([^\']*)\'(?:\s|$)', // 'attribute'
95
            '(\S+)(?:\s|$)', // attribute
96
        ]);
97 9
        $pattern = "/{$patterns}/";
98 9
        $text = preg_replace("/[\x{00a0}\x{200b}]+/u", ' ', $text);
99 9
        if (preg_match_all($pattern, (string) $text, $match, PREG_SET_ORDER)) {
100
101
            // Reject any unclosed HTML elements
102 7
            foreach ($this->generateAttributes($match) as $att => $value) {
103 7
                if ($value !== true && strpos((string) $value, '<') !== false) {
104 1
                    if (preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', (string) $value) !== 1) {
105 1
                        $value = '';
106
                    }
107
                }
108 7
                $atts[$att] = $value;
109
            }
110
        }
111
112 9
        return $atts;
113
    }
114
115
    /**
116
     * @param mixed[] $matches
117
     *
118
     * @return Generator<string|true>
119
     */
120 7
    private function generateAttributes(array $matches): Generator
121
    {
122 7
        foreach ($matches as $match) {
123 7
            $m = array_filter($match);
124 7
            $key = $m[1] ?? $m[3] ?? $m[5] ?? $m[7] ?? $m[8] ?? $m[9];
125 7
            $stringMatch = $m[2] ?? $m[4] ?? $m[6] ?? false;
126 7
            $value = $stringMatch ? stripcslashes($stringMatch) : true;
127 7
            yield strtolower($key) => $value;
128
        }
129 7
    }
130
131
    /**
132
     * @param mixed[] $matches
133
     *
134
     * @return Generator<mixed>
135
     */
136 1
    private function generateResults(array $matches): Generator
137
    {
138 1
        foreach ($matches as $match) {
139 1
            if ($match[1] == '[' && $match[6] == ']') {
140 1
                continue;
141
            }
142
            yield [
143 1
                'tag'        => $match[2],
144 1
                'content'    => isset($match[5]) ? $match[5] : null,
145 1
                'attributes' => isset($match[3]) ? $this->parseAttributes($match[3]) : [],
146
            ];
147
        }
148 1
    }
149
150
    /**
151
     * @param Closure $callback
152
     *
153
     * @return Closure
154
     */
155 1
    private function generateCallback(Closure $callback): Closure
156
    {
157
        return function ($match) use ($callback) {
158 1
            if ($match[1] == '[' && $match[6] == ']') {
159 1
                return substr($match[0], 1, -1);
160
            }
161
162 1
            $content = isset($match[5]) ? $match[5] : null;
163 1
            $atts = isset($match[3]) ? $this->parseAttributes($match[3]) : [];
164
165 1
            return $callback($match[2], $content, $atts);
166 1
        };
167
    }
168
}
169