Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHP regex, preg_replace_callback, failing with 2 possible matches in 1 string

Tags:

I was playing around with this very basic class to learn more about regexes. What I wanted to achieve is to grab the content from a testview file, grab the string exactly between {{EACH slides}} and {{#EACH}}. The regex now present in the class did just that, plus the variable name in the opening tag. I then wanted to use that string to loop it with the right variable and put it back in place.

But when I use 2 EACH-loops in my view instead of 1, it fails badly. The preg_replace_callback() will match the first opening-tag, and the last closing-tag of the second loop, and give a string something like this:

"htmlbalalbal {{#EACH}} more htmlblabla {{EACH newsitems}} htmlblalblabla"

From:

<div class="rows-container">
{{each slides}}
    <div class="row flex-wrap" data-id="{{id}}">
    </div>
{{#each}}
</div>
<div class="rows-container">
{{each blogposts}}
    <div class="row flex-wrap" data-id="{{id}}">
    </div>
{{#each}}

I've been trying the change the regex for quite some time now, and I'm starting to see more and more logic in it... but I have yet not solved my problem. Is it even possible to go down this path? or do I need another solution for scanning multiple EACH loops in my testfile? I will probably soon be using some framework, but I like doing this stuff when I'm bored during the weekends...

class Aclass{

    private $each = '~\{\{each ([^{]*)\}\}([^*]*)\{\{\#each\}\}~i';

    private $current_file_content = "";
    private $testviews = ['/testview.php'];

    private $views;

    function __construct($views){
        $this->views = $views; //not in use
        $this->start();
   }

   private function start(){
        foreach($this->testviews as $file){
            $this->current_file_content = file_get_contents(ROOT.$file);
            $this->scan_each();
        }
   }

   private function scan_each(){
       $result = preg_replace_callback($this->each, [$this, '_each'], $this->current_file_content);
   }


    private function _each($matches){
        foreach($matches as $match){
            var_dump(htmlspecialchars($match));
            echo '<br><br>';
        }
    }

Edit:16-5 So, I've been going further.. its working great, but it doesn't cover an each inside another each. It's a situation like this, which I think might be useful in the future:

{{each attributes}}
<label>{title}</label>
<select name="{key}" id="{key}" class="select">
    <option value="0" selected>choose an option</option>
    {{each values}}
        <option value="{id}" {selected}>{title}</option>
    {{#each}}
</select>
{{#each}}

I've been wondering if there is a way to cover it in 1 regex, and then if the function that processes the outer each-loop sees another each inside, it will finish that 1 first. I'm totaly willing to build the functions myself, but it would be awesome to get some tips. Would you recommend separating the regex? to find {{each X}} and {{#each}} and fix it another way in php, or fix the answered Regex to be able to handle this?

like image 218
Maarten Kuijper Avatar asked May 12 '17 19:05

Maarten Kuijper


1 Answers

Your regex can be fixed as

'~{{each\s+([^{}]*)}}(.*?){{#each}}~is'

See the regex demo.

The main point here is that [^*] you used in your pattern matches any character but * while you need to match any 0+ characters between one string and the other. It can be achieved with .*? and s modifier.

Details:

  • {{each - a literal substring
  • \s+ - 1+ whitespaces
  • ([^{}]*) - Group 1: zero or more chars other than { and }
  • }} - a literal }} substring
  • (.*?) - Group 2: any 0+ chars, as few as possible (due to the lazy *? quantifier
  • {{#each}} - a literal {{#each}} substring.
like image 125
Wiktor Stribiżew Avatar answered Sep 21 '22 10:09

Wiktor Stribiżew