1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Iterator;
38
39/**
40 * Class \Hoa\Iterator\RegularExpression.
41 *
42 * Re-implement the SPL RegexIterator class.
43 * There are too many bugs in php-src and HHVM, so we re-implement it from
44 * scratch without extending the existing class.
45 *
46 * Inspired by hhvm://hphp/system/php/spl/iterators/RegexIterator.php
47 *
48 * @copyright  Copyright © 2007-2017 Hoa community
49 * @license    New BSD License
50 */
51class RegularExpression extends Filter
52{
53    /**
54     * Flag: match the entry key instead of the entry value.
55     *
56     * @const int
57     */
58    const USE_KEY      = 1;
59
60    /**
61     * Flag: invert match.
62     *
63     * @const int
64     */
65    const INVERT_MATCH = 2;
66
67    /**
68     * Mode and preg flag: only execute match (filter) for the current entry.
69     *
70     * @const int
71     */
72    const MATCH        = 0;
73
74    /**
75     * Mode and preg flag: first match for the current entry.
76     *
77     * @const int
78     */
79    const GET_MATCH    = 1;
80
81    /**
82     * Mode and preg flag: all matches for the current entry.
83     *
84     * @const int
85     */
86    const ALL_MATCHES  = 2;
87
88    /**
89     * Mode and preg flag: split values for the current entry.
90     *
91     * @const int
92     */
93    const SPLIT        = 3;
94
95    /**
96     * Mode and preg flag: replace the current entry.
97     *
98     * @const int
99     */
100    const REPLACE      = 4;
101
102    /**
103     * The regular expression to match.
104     *
105     * @var string
106     */
107    protected $_regex     = null;
108
109    /**
110     * Operation mode, see the \RegexIterator::setMode method for a list of
111     * modes.
112     *
113     * @var int
114     */
115    protected $_mode      = 0;
116
117    /**
118     * Special flags, see the \RegexIterator::setFlag method for a list of
119     * available flags.
120     *
121     * @var int
122     */
123    protected $_flags     = 0;
124
125    /**
126     * The regular expression flags. See constants.
127     *
128     * @var int
129     */
130    protected $_pregFlags = 0;
131
132    /**
133     * Current key.
134     *
135     * @var int
136     */
137    protected $_key       = 0;
138
139    /**
140     * Current value.
141     *
142     * @var string
143     */
144    protected $_current   = null;
145
146    /**
147     * Replacement.
148     *
149     * @var string
150     */
151    public $replacement   = null;
152
153
154
155    /**
156     * Constructor.
157     *
158     * @param   \RecursiveIterator  $iterator     The recursive iterator to
159     *                                            apply this regex filter to.
160     * @param   string              $regex        The regular expression to
161     *                                            match.
162     * @param   int                 $mode         Operation mode, please see the
163     *                                            \RegexIterator::setMode method.
164     * @param   int                 $flags        Special flags, please see the
165     *                                            \RegexIterator::setFlags method.
166     * @param   int                 $pregFlags    Regular expression flags,
167     *                                            please see
168     *                                            \RegexIterator constants.
169     */
170    public function __construct(
171        \Iterator $iterator,
172        $regex,
173        $mode      = self::MATCH,
174        $flags     = 0,
175        $pregFlags = 0
176    ) {
177        parent::__construct($iterator);
178
179        $this->_regex = $regex;
180        $this->setMode($mode);
181        $this->setFlags($flags);
182        $this->setPregFlags($pregFlags);
183        $this->replacement = null;
184
185        return;
186    }
187
188    /**
189     * Get accept status.
190     *
191     * @return  bool
192     */
193    public function accept()
194    {
195        if (is_array(parent::current())) {
196            return false;
197        }
198
199        $this->_key     = parent::key();
200        $this->_current = parent::current();
201
202        $matches = [];
203        $useKey  = $this->_flags & self::USE_KEY;
204        $subject = $useKey ? $this->_key : $this->_current;
205        $out     = false;
206
207        switch ($this->_mode) {
208
209            case self::MATCH:
210                $out = 0 !== preg_match(
211                    $this->_regex,
212                    $subject,
213                    $matches,
214                    $this->_pregFlags
215                );
216
217                break;
218
219            case self::GET_MATCH:
220                $this->_current = [];
221                $out            = 0 !== preg_match(
222                    $this->_regex,
223                    $subject,
224                    $this->_current,
225                    $this->_pregFlags
226                );
227
228                break;
229
230            case self::ALL_MATCHES:
231                $this->_current = [];
232                $out            = 0 < preg_match_all(
233                    $this->_regex,
234                    $subject,
235                    $this->_current,
236                    $this->_pregFlags
237                );
238
239                break;
240
241            case self::SPLIT:
242                $this->_current = preg_split(
243                    $this->_regex,
244                    $subject,
245                    null,
246                    $this->_pregFlags
247                );
248
249                $out =
250                    is_array($this->_current) &&
251                    1 < count($this->_current);
252
253                break;
254
255            case self::REPLACE:
256                $numberOfReplacement = 0;
257                $result              = preg_replace(
258                    $this->_regex,
259                    $this->replacement,
260                    $subject,
261                    -1,
262                    $numberOfReplacement
263                );
264
265                if (null === $result || 0 === $numberOfReplacement) {
266                    $out = false;
267
268                    break;
269                }
270
271                if (0 !== $useKey) {
272                    $this->_key = $result;
273                    $out        = true;
274
275                    break;
276                }
277
278                $this->_current = $result;
279                $out            = true;
280
281                break;
282
283            default:
284                $out = false;
285
286                break;
287        }
288
289        if (0 !== ($this->_flags & self::INVERT_MATCH)) {
290            return false === $out;
291        }
292
293        return $out;
294    }
295
296    /**
297     * Get current key.
298     *
299     * @return  int
300     */
301    public function key()
302    {
303        return $this->_key;
304    }
305
306    /**
307     * Get current value.
308     *
309     * @return  string
310     */
311    public function current()
312    {
313        return $this->_current;
314    }
315
316    /**
317     * Set mode.
318     *
319     * @param   int  $mode   Mode.
320     * @return  void
321     */
322    public function setMode($mode)
323    {
324        if ($mode < self::MATCH || $mode > self::REPLACE) {
325            throw new \InvalidArgumentException(
326                'Illegal mode ' . $mode . '.'
327            );
328        }
329
330        $this->_mode = $mode;
331
332        return;
333    }
334
335    /**
336     * Set flags.
337     *
338     * @param   int  $flags    Flags.
339     * @return  void
340     */
341    public function setFlags($flags)
342    {
343        $this->_flags = $flags;
344
345        return;
346    }
347
348    /**
349     * Set preg flags.
350     *
351     * @param   int  $pregFlags    Preg flags.
352     * @return  void
353     */
354    public function setPregFlags($pregFlags)
355    {
356        $this->_pregFlags = $pregFlags;
357
358        return;
359    }
360
361    /**
362     * Get regular expression.
363     *
364     * @return  string
365     */
366    public function getRegex()
367    {
368        return $this->_regex;
369    }
370
371    /**
372     * Get mode.
373     *
374     * @return  int
375     */
376    public function getMode()
377    {
378        return $this->_mode;
379    }
380
381    /**
382     * Get flags.
383     *
384     * @return  int
385     */
386    public function getFlags()
387    {
388        return $this->_flags;
389    }
390
391    /**
392     * Get preg flags.
393     *
394     * @return  int
395     */
396    public function getPregFlags()
397    {
398        return $this->_pregFlags;
399    }
400}
401