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

How to match regex occurences from the top to bottom

出于学习原因,我正在尝试创建一个 PHP 模板引擎。

假设我们有以下 array:

$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>
  }}
}} ';

思路是从上到下读取字符串,依靠正则表达式的列表找到结果并给出到正确的函数.

例如:首先要查找的必须是{{$string}},所以我们将变量名称传递给getVar($matchedVarName)函数.

下一个必须是@loop(){{ }},所以,我们调用loop($matchedArrayName);

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

我怎样才能以正确的顺序(从上到下)执行此操作?谢谢。

如果您为 preg_match_all 使用 PREG_OFFSET_CAPTURE 标志,这是可能的。该标志的作用是更改捕获的匹配项以将偏移量包含在模式字符串中。现在,我们唯一需要做的就是按照偏移量指示的相同顺序遍历所有匹配项。

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

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

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

      如果您愿意为所有匹配项调用相同的函数,我们可以将所有匹配项合并到一个数组中并按偏移量对它们进行排序。

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

在这一点上,我应该指出,您示例中的最后两个正则表达式根本不匹配。这是因为在循环和语句中有 {} 的实例。为了演示,我简单地截掉了一部分正则表达式,所以它们只匹配 loop/statement.

的条件

下面的脚本将打印这个。

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]++;
}