Passed
Branch version-bump (36e4b3)
by Matt
04:39
created

DefaultParser   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 145
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 31
eloc 82
dl 0
loc 145
c 0
b 0
f 0
rs 9.92

4 Methods

Rating   Name   Duplication   Size   Complexity  
A getRegex() 0 33 1
B parseShortcode() 0 24 9
C parseAttributes() 0 44 15
A generateResults() 0 10 6
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
    public function parseShortcode(string $content, array $tags, Closure $callback = null)
21
    {
22
        if (strpos($content, '[') === false && empty($tags)) {
23
            return is_null($callback) ? [] : $content;
24
        }
25
26
        $regex = $this->getRegex($tags);
27
28
        preg_match_all("/$regex/", $content, $matches, PREG_SET_ORDER);
29
30
        if (is_null($callback)) {
31
            return iterator_to_array($this->generateResults($matches));
32
        }
33
34
        return preg_replace_callback("/$regex/", function ($match) use ($callback) {
35
            if ($match[1] == '[' && $match[6] == ']') {
36
                return substr($match[0], 1, -1);
37
            }
38
39
            $content = isset($match[5]) ? $match[5] : null;
40
            $atts = isset($match[3]) ? $this->parseAttributes($match[3]) : [];
41
42
            return $callback($match[2], $content, $atts);
43
        }, $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
    private function getRegex(array $tags): string
54
    {
55
        $tagregexp = implode('|', array_map('preg_quote', $tags));
56
57
        return
58
            '\\['                // Opening bracket
59
            .'(\\[?)'           // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
60
            ."($tagregexp)"     // 2: Shortcode name
61
            .'(?![\\w-])'       // Not followed by word character or hyphen
62
            .'('                // 3: Unroll the loop: Inside the opening shortcode tag
63
            .'[^\\]\\/]*'       // Not a closing bracket or forward slash
64
            .'(?:'
65
            .'\\/(?!\\])'       // A forward slash not followed by a closing bracket
66
            .'[^\\]\\/]*'       // Not a closing bracket or forward slash
67
            .')*?'
68
            .')'
69
            .'(?:'
70
            .'(\\/)'            // 4: Self closing tag ...
71
            .'\\]'              // ... and closing bracket
72
            .'|'
73
            .'\\]'              // Closing bracket
74
            .'(?:'
75
            .'('                // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
76
            .'[^\\[]*+'         // Not an opening bracket
77
            .'(?:'
78
            .'\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag
79
            .'[^\\[]*+'         // Not an opening bracket
80
            .')*+'
81
            .')'
82
            .'\\[\\/\\2\\]'     // Closing shortcode tag
83
            .')?'
84
            .')'
85
            .'(\\]?)';          // 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
    public function parseAttributes(string $text): array
96
    {
97
        $atts = [];
98
        $patterns = implode('|', [
99
            '([\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
        $pattern = "/{$patterns}/";
107
        $text = preg_replace("/[\x{00a0}\x{200b}]+/u", ' ', $text);
108
        if (preg_match_all($pattern, (string) $text, $match, PREG_SET_ORDER)) {
109
            foreach ($match as $m) {
110
                if (!empty($m[1])) {
111
                    $atts[strtolower($m[1])] = stripcslashes($m[2]);
112
                } elseif (!empty($m[3])) {
113
                    $atts[strtolower($m[3])] = stripcslashes($m[4]);
114
                } elseif (!empty($m[5])) {
115
                    $atts[strtolower($m[5])] = stripcslashes($m[6]);
116
                } elseif (isset($m[7]) && strlen($m[7])) {
117
                    $atts[strtolower($m[7])] = true;
118
                } elseif (isset($m[8]) && strlen($m[8])) {
119
                    $atts[strtolower($m[8])] = true;
120
                } elseif (isset($m[9])) {
121
                    $atts[strtolower($m[9])] = true;
122
                }
123
            }
124
125
            // Reject any unclosed HTML elements
126
            foreach ($atts as &$value) {
127
                if (!is_string($value)) {
128
                    continue;
129
                }
130
                if (strpos($value, '<') !== false) {
131
                    if (1 !== preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $value)) {
132
                        $value = '';
133
                    }
134
                }
135
            }
136
        }
137
138
        return $atts;
139
    }
140
141
    /**
142
     * @param array $matches
143
     *
144
     * @return Generator
145
     */
146
    private function generateResults(array $matches): Generator
147
    {
148
        foreach ($matches as $match) {
149
            if ($match[1] == '[' && $match[6] == ']') {
150
                continue;
151
            }
152
            yield [
153
                'tag'        => $match[2],
154
                'content'    => isset($match[5]) ? $match[5] : null,
155
                'attributes' => isset($match[3]) ? $this->parseAttributes($match[3]) : [],
156
            ];
157
        }
158
    }
159
}
160