如何匹配从上到下的正则表达式

时间:2017-05-24 13:56:54

标签: php regex recursion template-engine

我出于学习原因尝试创建PHP模板引擎。

假设我们有以下数组

$regexList = [
  'varPattern' => '/{{\s*\$(.*?)\s*}}/',
  'loopPattern' => '/@for\((.*?)\)\s*{{((?:[^{}]|(?R))*)}}/',
  'statementPattern' => '/@if\((.*?)\)\s*{{((?:[^{}]|(?R))*)}}/'
]

和以下功能

getVar($varName);
loop($arrayName);
getStatementResult($booleanExpression);

字符串如下:

$string = '

<span>{{ $fullName }}</span>

@for($names as $name)
{{
  @if($name == 'Eleandro)
  {{
    <p>{{ $name }}</p>
  }}
}} ';

我的想法是从上到下阅读字符串并依赖正则表达式列表找到结果并将其提供给正确的函数< / em>的

例如:要查找的第一件事必须是{{$string}},因此我们将变量名称传递给getVar($matchedVarName) 函数。< / p>

下一步必须是@loop(){{ }},因此,我们致电loop($matchedArrayName);

并且循环内部必须找到@if(){{ }},因此,我们得到结果并将getStatementResult($matchedBooleanExpression)提供给匹配的值。

我如何以正确的顺序(从上到下)执行此操作?谢谢。

1 个答案:

答案 0 :(得分:1)

如果您对preg_match_all使用 PREG_OFFSET_CAPTURE 标记,则可以执行此操作。该标志将执行的操作是更改捕获的匹配项以包括模式字符串中的偏移量。现在,我们唯一需要做的就是以与偏移量相同的顺序遍历所有匹配。

这有点棘手,但绝对有可能。我的方法如下:

  1. 为每个正则表达式创建匹配数组。由于您有三个正则表达式,因此这将是一个包含三个值的数组。对于该正则表达式,每个值只是由$matches匹配的preg_match_all 重要提示:匹配按每个数组中的偏移量排序。
  2. 为每个正则表达式创建一个索引数组。我们将使用它来跟踪每个正则表达式的位置。这被初始化为[0, 0, 0]。为了使它更通用并让它处理任意数量的正则表达式,我使用array_fill
  3. 完成了这项工作
  4. 虽然并非所有比赛都得到处理......

    1. 确定要处理的下一个匹配项。这将是下一个匹配具有最小偏移量的正则表达式。我为此写了一个小minIndex函数。
    2. 使用匹配组的值调用相应的函数(在$functions下硬编码)。此时,我们知道要调用哪个函数,因为我们知道哪个正则表达式创建了这个特定的匹配。

      如果你可以为所有匹配调用相同的函数,我们可以在一个数组中合并所有匹配并按偏移量排序。

    3. 增加匹配的正则表达式的索引。

  5. 此时,我应该注意您示例中的最后两个正则表达式根本不匹配。这是因为循环和语句中存在{}的实例。为了演示,我简单地删除了那些正则表达式的一部分,因此它们只匹配循环/语句的条件。

    以下脚本将打印出来。

    getVar('fullName')
    loop('$names as $name')
    getStatementResult('$name === "Eleandro"')
    getVar('name')
    

    我相信,这是你期望的输出。

    // functions
    function getVar($varName) {
        echo "getVar('$varName')\n";
    }
    
    function loop($arrayName) {
        echo "loop('$arrayName')\n";
    }
    
    function getStatementResult($booleanExpression) {
        echo "getStatementResult('$booleanExpression')\n";
    }
    
    // input
    $string = '
    
    <span>{{ $fullName }}</span>
    
    @for($names as $name)
    {{
      @if($name === "Eleandro")
      {{
        <p>{{ $name }}</p>
      }}
    }} ';
    
    // helper functions
    function minIndex($arr) {
        $i = 0; $l = count($arr);
        $min = false; $minI = -1;
        for ($i = 0; $i < $l; ++$i) {
            if ($arr[$i] === false)  continue; // skip non numbers
            if ($min === false || $arr[$i] < $min) {
                $min = $arr[$i];
                $minI = $i;
            }
        }
        return $minI;
    }
    
    // regular expressions
    $regexList = [
        'varPattern' => '/{{\s*\$(.*?)\s*}}/',
        'loopPattern' => '/@for\((.*?)\)\s*{{/',
        'statementPattern' => '/@if\((.*?)\)\s*{{/'
    ];
    
    // functions to map above regexes to
    $functions = ['getVar', 'loop', 'getStatementResult'];
    // matches per regex
    $matchesAll = [];
    
    // combine the above regexes into a single one, run that
    foreach ($regexList as $name => $regex) {
        unset($matches);
        preg_match_all($regex, $string, $matches, PREG_OFFSET_CAPTURE);
        $matchesAll[] = $matches;
    }
    
    // walk over the matches in order of offset in string
    // current match per regex
    $indexes = array_fill(0, count($regexList), 0);
    // number of matches per regex
    $counts = array_map(function($m) { return count($m[0]); }, $matchesAll);
    while ($indexes !== $counts) {
        $offsets = array_map(function($m, $i) {
                return (count($m[0]) > $i ? $m[0][$i][1] : false);
            }, $matchesAll, $indexes);
        $next = minIndex($offsets);
        call_user_func($functions[$next],
            $matchesAll[$next][1][$indexes[$next]][0]);
        $indexes[$next]++;
    }