Completed
Push — master ( 3ad8df...af667b )
by Matt
03:42
created

DefaultParser::generateAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 8
ccs 7
cts 7
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3
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 ($value !== true && strpos((string)$value, '<') !== false) {
113 1
                    if (preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', (string)$value) !== 1) {
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 $match) {
132 7
            $m = array_filter($match);
133 7
            $key = $m[1] ?? $m[3] ?? $m[5] ?? $m[7] ?? $m[8] ?? $m[9];
134 7
            $stringMatch = $m[2] ?? $m[4] ?? $m[6] ?? false;
135 7
            $value = $stringMatch ? stripcslashes($stringMatch) : true;
136 7
            yield strtolower($key) => $value;
137
        }
138 7
    }
139
140
    /**
141
     * @param array $matches
142
     *
143
     * @return Generator
144
     */
145 1
    private function generateResults(array $matches): Generator
146
    {
147 1
        foreach ($matches as $match) {
148 1
            if ($match[1] == '[' && $match[6] == ']') {
149 1
                continue;
150
            }
151
            yield [
152 1
                'tag'        => $match[2],
153 1
                'content'    => isset($match[5]) ? $match[5] : null,
154 1
                'attributes' => isset($match[3]) ? $this->parseAttributes($match[3]) : [],
155
            ];
156
        }
157 1
    }
158
}
159