EDIT
Я переписал код! Теперь он содержит изменения, перечисленные ниже. Кроме того, я провел обширные тесты (которые я не буду публиковать здесь, потому что их слишком много), чтобы найти ошибки. До сих пор я не нашел ничего.
Функция теперь разделена на две части: есть отдельная функция preg_split
, которая принимает регулярное выражение и возвращает массив, содержащий пустое выражение (без разделителей) и массив модификаторов. Это может пригодиться (на самом деле это уже произошло; вот почему я сделал это изменение).
Код теперь корректно обрабатывает обратные ссылки. В конце концов, это было необходимо для моей цели. Это было несложно добавить, регулярное выражение, используемое для захвата обратных ссылок, выглядит странно (и на самом деле может быть крайне неэффективным, мне кажется, что это сложно для NP - но это только интуиция и применяется только в странных крайних случаях) , Кстати, кто-нибудь знает лучший способ проверки на неравное количество матчей, чем мой путь? Отрицательные lookbehinds не будут работать здесь, потому что они принимают только строки фиксированной длины вместо регулярных выражений. Однако мне нужно здесь регулярное выражение, чтобы проверить, действительно ли предыдущий обратный слеш экранирован сам по себе.
Кроме того, я не знаю, насколько хорош PHP для кэширования анонимного create_function
использования. С точки зрения производительности это может быть не лучшим решением, но оно кажется достаточно хорошим.
Я исправил ошибку в проверке работоспособности.
Я удалил отмену устаревших модификаторов, так как мои тесты показали, что в этом нет необходимости.
Кстати, этот код является одним из основных компонентов подсветки синтаксиса для различных языков, над которыми я работаю в PHP, поскольку меня не устраивают перечисленные альтернативы в других местах .
Спасибо!
porneL , без век , потрясающая работа! Большое большое спасибо. Я на самом деле сдался.
Я опирался на ваше решение, и я хотел бы поделиться им здесь. Я не реализовал повторную нумерацию обратных ссылок, поскольку в моем случае это не актуально (я думаю…). Возможно, это станет необходимым позже.
Некоторые вопросы ...
Одна вещь, @ без век : Почему вы чувствуете необходимость отменить старые модификаторы? Насколько я понимаю, в этом нет необходимости, поскольку модификаторы применяются только локально.
Ах да, еще одна вещь. Ваше спасение от разделителя кажется слишком сложным. Не могли бы вы объяснить, почему вы считаете это необходимым? Я верю, что моя версия тоже должна работать, но я могу ошибаться.
Кроме того, я изменил подпись вашей функции в соответствии с моими потребностями. Я также считаю, что моя версия более полезна. Опять же, я могу ошибаться.
Кстати, теперь вы должны понимать важность настоящих имен для SO. ;-) Я не могу дать вам реальный кредит в коде. : - /
Код
В любом случае, я бы хотел поделиться своим результатом, потому что я не могу поверить, что никому больше не нужно что-то подобное. Код кажется работает очень хорошо. Обширные тесты еще предстоит сделать. Пожалуйста, прокомментируйте!
И без лишних слов ...
/**
* Merges several regular expressions into one, using the indicated 'glue'.
*
* This function takes care of individual modifiers so it's safe to use
* <em>different</em> modifiers on the individual expressions. The order of
* sub-matches is preserved as well. Numbered back-references are adapted to
* the new overall sub-match count. This means that it's safe to use numbered
* back-refences in the individual expressions!
* If {@link $names} is given, the individual expressions are captured in
* named sub-matches using the contents of that array as names.
* Matching pair-delimiters (e.g. <code>"{…}"</code>) are currently
* <strong>not</strong> supported.
*
* The function assumes that all regular expressions are well-formed.
* Behaviour is undefined if they aren't.
*
* This function was created after a {@link /191163/obedinenie-regulyarnyh-vyrazhenii-v-php
* StackOverflow discussion}. Much of it was written or thought of by
* “porneL” and “eyelidlessness”. Many thanks to both of them.
*
* @param string $glue A string to insert between the individual expressions.
* This should usually be either the empty string, indicating
* concatenation, or the pipe (<code>|</code>), indicating alternation.
* Notice that this string might have to be escaped since it is treated
* like a normal character in a regular expression (i.e. <code>/</code>)
* will end the expression and result in an invalid output.
* @param array $expressions The expressions to merge. The expressions may
* have arbitrary different delimiters and modifiers.
* @param array $names Optional. This is either an empty array or an array of
* strings of the same length as {@link $expressions}. In that case,
* the strings of this array are used to create named sub-matches for the
* expressions.
* @return string An string representing a regular expression equivalent to the
* merged expressions. Returns <code>FALSE</code> if an error occurred.
*/
function preg_merge($glue, array $expressions, array $names = array()) {
// … then, a miracle occurs.
// Sanity check …
$use_names = ($names !== null and count($names) !== 0);
if (
$use_names and count($names) !== count($expressions) or
!is_string($glue)
)
return false;
$result = array();
// For keeping track of the names for sub-matches.
$names_count = 0;
// For keeping track of *all* captures to re-adjust backreferences.
$capture_count = 0;
foreach ($expressions as $expression) {
if ($use_names)
$name = str_replace(' ', '_', $names[$names_count++]);
// Get delimiters and modifiers:
$stripped = preg_strip($expression);
if ($stripped === false)
return false;
list($sub_expr, $modifiers) = $stripped;
// Re-adjust backreferences:
// We assume that the expression is correct and therefore don't check
// for matching parentheses.
$number_of_captures = preg_match_all('/\([^?]|\(\?[^:]/', $sub_expr, $_);
if ($number_of_captures === false)
return false;
if ($number_of_captures > 0) {
// NB: This looks NP-hard. Consider replacing.
$backref_expr = '/
( # Only match when not escaped:
[^\\\\] # guarantee an even number of backslashes
(\\\\*?)\\2 # (twice n, preceded by something else).
)
\\\\ (\d) # Backslash followed by a digit.
/x';
$sub_expr = preg_replace_callback(
$backref_expr,
create_function(
'$m',
'return $m[1] . "\\\\" . ((int)$m[3] + ' . $capture_count . ');'
),
$sub_expr
);
$capture_count += $number_of_captures;
}
// Last, construct the new sub-match:
$modifiers = implode('', $modifiers);
$sub_modifiers = "(?$modifiers)";
if ($sub_modifiers === '(?)')
$sub_modifiers = '';
$sub_name = $use_names ? "?<$name>" : '?:';
$new_expr = "($sub_name$sub_modifiers$sub_expr)";
$result[] = $new_expr;
}
return '/' . implode($glue, $result) . '/';
}
/**
* Strips a regular expression string off its delimiters and modifiers.
* Additionally, normalize the delimiters (i.e. reformat the pattern so that
* it could have used '/' as delimiter).
*
* @param string $expression The regular expression string to strip.
* @return array An array whose first entry is the expression itself, the
* second an array of delimiters. If the argument is not a valid regular
* expression, returns <code>FALSE</code>.
*
*/
function preg_strip($expression) {
if (preg_match('/^(.)(.*)\\1([imsxeADSUXJu]*)$/s', $expression, $matches) !== 1)
return false;
$delim = $matches[1];
$sub_expr = $matches[2];
if ($delim !== '/') {
// Replace occurrences by the escaped delimiter by its unescaped
// version and escape new delimiter.
$sub_expr = str_replace("\\$delim", $delim, $sub_expr);
$sub_expr = str_replace('/', '\\/', $sub_expr);
}
$modifiers = $matches[3] === '' ? array() : str_split(trim($matches[3]));
return array($sub_expr, $modifiers);
}
PS: я сделал эту публикацию вики-сообщества доступной для редактирования. Вы знаете, что это значит ...!