У меня есть веб-служба, которая перезаписывает URL-адреса в css-файлах, чтобы их можно было обслуживать через CDN.
Файлы css могут содержать URL-адреса для изображений или шрифтов.
В настоящее время у меня есть следующее регулярное выражение для соответствия всем URL-адресам в файле css:
(url\(\s*([\'\"]?+))((?!(https?\:|data\:|\.\.\/|\/))\S+)((\2)\s*\))
Однако теперь я хочу представить поддержку пользовательских шрифтов и настроить таргетинг на URL-адреса в @font-fontface
:
@font-face { font-family: 'FontAwesome'; src: url("fonts/fontawesome-webfont.eot?v=4.0.3"); src: url("fonts/fontawesome-webfont.eot?#iefix&v=4.0.3") format("embedded-opentype"), url("fonts/fontawesome-webfont.woff?v=4.0.3") format("woff"), url("fonts/fontawesome-webfont.ttf?v=4.0.3") format("truetype"), url("fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular") format("svg"); font-weight: normal; font-style: normal; }
Затем я придумал следующее:
@font-face\s*\{.*(url\(\s*([\'\"]?+))((?!(https?\:|data\:|\.\.\/|\/))\S+)((\2)\s*\))\s*\}
Проблема в том, что это соответствует всему, а не только URL внутри. Я думал, что могу использовать lookbehind так:
(?<=@font-face\s*\{.*)(url\(\s*([\'\"]?+))((?!(https?\:|data\:|\.\.\/|\/))\S+)((\2)\s*\))(?<=-\s*\})
К сожалению, PCRE (который использует PHP) не поддерживает переменное повторение в lookbehind, поэтому я застрял.
Я не хочу проверять шрифты по их расширению, поскольку некоторые шрифты имеют расширение .svg
которое может конфликтовать с изображениями с расширением .svg
.
Кроме того, я также хотел бы изменить свое исходное регулярное выражение, чтобы соответствовать всем другим URL-адресам, которые НЕ находятся внутри @font-face
:
.someclass { background: url('images/someimage.png') no-repeat; }
Поскольку я не могу использовать lookbehinds, как я могу извлечь URL-адреса из тех, что находятся в @font-face
и те, которые не находятся в @font-face
?
Вы можете использовать это:
$pattern = <<<'LOD' ~ (?(DEFINE) (?<quoted_content> (["']) (?>[^"'\\]++ | \\{2} | \\. | (?!\g{-1})["'] )*+ \g{-1} ) (?<comment> /\* .*? \*/ ) (?<url_skip> (?: https?: | data: ) [^"'\s)}]*+ ) (?<other_content> (?> [^u}/"']++ | \g<quoted_content> | \g<comment> | \Bu | u(?!rl\s*+\() | /(?!\*) | \g<url_start> \g<url_skip> ["']?+ )++ ) (?<anchor> \G(?<!^) ["']?+ | @font-face \s*+ { ) (?<url_start> url\( \s*+ ["']?+ ) ) \g<comment> (*SKIP)(*FAIL) | \g<anchor> \g<other_content>?+ \g<url_start> \K [./]*+ ( [^"'\s)}]*+ ) # url ~xs LOD; $result = preg_replace($pattern, 'http://cdn.test.com/fonts/$8', $data); print_r($result);
тестовая строка
$data = <<<'LOD' @font-face { font-family: 'FontAwesome'; src: url("fonts/fontawesome-webfont.eot?v=4.0.3"); src: url(fonts/fontawesome-webfont.eot?#iefix&v=4.0.3) format("embedded-opentype"), /*url("fonts/fontawesome-webfont.woff?v=4.0.3") format("woff"),*/ url("http://domain.com/fonts/fontawesome-webfont.ttf?v=4.0.3") format("truetype"), url('fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format("svg"); font-weight: normal; font-style: normal; } /* @font-face { font-family: 'Font1'; src: url("fonts/font1.eot"); } */ @font-face { font-family: 'Fon\'t2'; src: url("fonts/font2.eot"); } @font-face { font-family: 'Font3'; src: url("../fonts/font3.eot"); } LOD;
Для большей удобочитаемости шаблон разделяется на именованные подшаблоны. (?(DEFINE)...)
ничего не соответствует, это только раздел определения.
Основной трюк этого шаблона – использование привязки \G
что означает: начало строки или смежное совпадение с прецедентом . Я добавил отрицательный lookbehind (?<!^)
Чтобы избежать первой части этого определения.
<anchor>
named subpattern является наиболее важным, потому что он позволяет совпадение, только если @font-face {
найден или сразу после конца URL-адреса (вот почему вы можете видеть ["']?+
).
<other_content>
представляет все, что не является частью URL- адреса, но соответствует разделам url, которые также должны быть пропущены (URL-адреса, начинающиеся с «http:», «data:») . Важная деталь этого подшаблона заключается в том, что он не может соответствовать закрывающей фигурной скобке @ font-face.
Миссия <url_start>
заключается только в сопоставлении url("
.
\K
сбрасывает все подстроки, которые были сопоставлены до результата результата.
([^"'\s)}]*+)
соответствует URL-адресу (единственное, что осталось в результате совпадения с ведущим ./../
)
Поскольку <other_content>
и подшаблон url не могут сопоставляться с a }
(это внешние цитаты или комментарии), вы наверняка никогда не будете соответствовать чему-либо вне определения @ font-face, второе последствие состоит в том, что шаблон всегда терпит неудачу после последний URL. Таким образом, при следующей попытке «смежная ветка» провалится до следующего @ font-face.
Основной шаблон начинается с \g<comment> (*SKIP)(*FAIL) |
пропустить весь контент внутри комментариев /*....*/
. \g<comment>
относится к базовому подшаблону, который описывает, как выглядит комментарий. (*SKIP)
запрещает повторять подстроку, которая была сопоставлена раньше (слева от нее, g<comment>
), если паттерн не работает с правой стороны. (*FAIL)
заставляет шаблон терпеть неудачу. С помощью этого трюка комментарии пропущены и не соответствуют результатам (поскольку шаблон не работает).
quoted_content: Используется в <other_content>
чтобы избежать совпадения url(
или /*
которые находятся внутри кавычек.
(["']) # capture group: the opening quote (?> # atomic group: all possible content between quotes [^"'\\]++ # all that is not a quote or a backslash | # OR \\{2} # two backslashes: (two \ doesn't escape anything) | # OR \\. # any escaped character | # OR (?!\g{-1})["'] # the other quote (this one that is not in the capture group) )*+ # repeat zero or more time the atomic group \g{-1} # backreference to the last capturing group
other_content: все, что не является закрывающей фигурной скобкой, или URL-адресом без http:
или data:
(?> # open an atomic group [^u}/"']++ # all character that are not problematic! | \g<quoted_content> # string inside quotes | \g<comment> # string inside comments | \Bu # "u" not preceded by a word boundary | u(?!rl\s*+\() # "u" not followed by "rl(" (not the start of an url definition) | /(?!\*) # "/" not followed by "*" (not the start of a comment) | \g<url_start> # match the url that begins with "http:" \g<url_skip> ["']?+ # until the possible quote )++ # repeat the atomic group one or more times
якорь
\G(?<!^) ["']?+ # contiguous to a precedent match with a possible closing quote | # OR @font-face \s*+ { # start of the @font-face definition
Вы можете улучшить основной шаблон:
После последнего URL-адреса @ font-face, механизм регулярных выражений пытается совместить с «непрерывной ветвью» <anchor>
и сопоставлять все символы до тех пор, пока не будет }
, из-за чего шаблон не сработает. Затем, для каждого одинакового символа, движок регулярных выражений должен попробовать две ветви или <anchor>
(это всегда будет терпеть неудачу до }
.
Чтобы избежать этих бесполезных попыток, вы можете изменить основной шаблон на:
\g<comment> (*SKIP)(*FAIL) | \g<anchor> \g<other_content>?+ (?> \g<url_start> \K [./]*+ ([^"'\s)}]*+) | } (*SKIP)(*FAIL) )
С помощью этого нового сценария первый символ после последнего URL- \g<other_content>
совпадает с «смежной ветвью», \g<other_content>
соответствует всем, до тех пор, \g<url_start>
не \g<url_start>
немедленное выполнение }
, \g<url_start>
, }
и (*SKIP)(*FAIL)
делают шаблон неудачным и запрещают повторять эти символы.
Отказ от ответственности . Возможно, вы используете библиотеку, потому что она сложнее, чем вы думаете. Я также хочу начать этот ответ о том, как сопоставить URL-адреса, которые не находятся в пределах @ font-face {} . Я также предполагаю / определим, что скобки {} сбалансированы внутри @ font-face {} .
Примечание . Я использую «~» в качестве разделителей вместо «/», это освободит меня от последующего использования в моих выражениях. Также обратите внимание, что я буду размещать онлайн-демо от regex101.com , на этом сайте я буду использовать модификатор g . Вам следует удалить модификатор g и просто использовать preg_match_all () .
Давайте использовать некоторое регулярное выражение Фу !!!
О да, это может показаться «странным», но позже вы заметите, почему 🙂
Нам понадобится рекурсивное регулярное выражение:
@font-face\s* # Match @font-face and some spaces ( # Start group 1 \{ # Match { (?: # A non-capturing group [^{}]+ # Match anything except {} one or more times | # Or (?1) # Recurse/rerun the expression of group 1 )* # Repeat 0 or more times \} # Match } ) # End group 1
демонстрация
Мы будем использовать (*SKIP)(*FAIL)
сразу после предыдущего регулярного выражения, он пропустит его. См. Этот ответ, чтобы понять, как это работает.
демонстрация
Мы будем использовать что-то вроде этого:
url\s*\( # Match url, optionally some whitespaces and then ( \s* # Match optionally some whitespaces ("|'|) # It seems that the quotes are optional according to http://www.w3.org/TR/CSS2/syndata.html#uri (?!["']?(?:https?://|ftp://)) # Put your negative-rules here (do not match url's with http, https or ftp) (?:[^\\]|\\.)*? # Match anything except a backslash or backslash and a character zero or more times ungreedy \2 # Match what was matched in group 2 \s* # Match optionally some whitespaces \) # Match )
Обратите внимание, что я использую \2
потому что я добавил это к предыдущему регулярному выражению, которое имеет группу 1.
Вот еще одно использование ("|')(?:[^\\]|\\.)*?\1
.
демонстрация
Возможно, вы догадались, что нам нужно использовать некоторый lookaround-fu, проблема с lookbehind, поскольку она должна быть фиксированной длиной. У меня есть обходной путь для этого, я расскажу вам о escape-последовательности \K
Он сбрасывает начало совпадения на текущую позицию в списке токенов. больше информации
Ну, давайте качаем \K
где-то в нашем выражении и используем lookahead, наше окончательное регулярное выражение будет:
@font-face\s* # Match @font-face and some spaces ( # Start group 1 \{ # Match { (?: # A non-capturing group [^{}]+ # Match anything except {} one or more times | # Or (?1) # Recurse/rerun the expression of group 1 )* # Repeat 0 or more times \} # Match } ) # End group 1 (*SKIP)(*FAIL) # Skip it | # Or url\s*\( # Match url, optionally some whitespaces and then ( \s* # Match optionally some whitespaces ("|'|) # It seems that the quotes are optional according to http://www.w3.org/TR/CSS2/syndata.html#uri \K # Reset the match (?!["']?(?:https?://|ftp://)) # Put your negative-rules here (do not match url's with http, https or ftp) (?:[^\\]|\\.)*? # Match anything except a backslash or backslash and a character zero or more times ungreedy (?= # Lookahead \2 # Match what was matched in group 2 \s* # Match optionally some whitespaces \) # Match ) )
демонстрация
Нам нужно избежать некоторых вещей, таких как кавычки, обратные \\\\
= \
, использовать правильную функцию и правые модификаторы:
$regex = '~ @font-face\s* # Match @font-face and some spaces ( # Start group 1 \{ # Match { (?: # A non-capturing group [^{}]+ # Match anything except {} one or more times | # Or (?1) # Recurse/rerun the expression of group 1 )* # Repeat 0 or more times \} # Match } ) # End group 1 (*SKIP)(*FAIL) # Skip it | # Or url\s*\( # Match url, optionally some whitespaces and then ( \s* # Match optionally some whitespaces ("|\'|) # It seems that the quotes are optional according to http://www.w3.org/TR/CSS2/syndata.html#uri \K # Reset the match (?!["\']?(?:https?://|ftp://)) # Put your negative-rules here (do not match url's with http, https or ftp) (?:[^\\\\]|\\\\.)*? # Match anything except a backslash or backslash and a character zero or more times ungreedy (?= # Lookahead \2 # Match what was matched in group 2 \s* # Match optionally some whitespaces \) # Match ) ) ~xs'; $input = file_get_contents($css_file); preg_match_all($regex, $input, $m); echo '<pre>'. print_r($m[0], true) . '</pre>';
демонстрация
Я хочу сделать эту часть в 2 регулярных выражениях, потому что будет больно сопоставлять URL-адреса, которые находятся внутри @font-face {}
то же время заботясь о состоянии фигурных скобок {}
в рекурсивном регулярном выражении.
И поскольку у нас уже есть нужные нам фрагменты, нам нужно будет их применять только в некотором коде:
@font-face {}
$results = array(); // Just an empty array; $fontface_regex = '~ @font-face\s* # Match @font-face and some spaces ( # Start group 1 \{ # Match { (?: # A non-capturing group [^{}]+ # Match anything except {} one or more times | # Or (?1) # Recurse/rerun the expression of group 1 )* # Repeat 0 or more times \} # Match } ) # End group 1 ~xs'; $url_regex = '~ url\s*\( # Match url, optionally some whitespaces and then ( \s* # Match optionally some whitespaces ("|\'|) # It seems that the quotes are optional according to http://www.w3.org/TR/CSS2/syndata.html#uri \K # Reset the match (?!["\']?(?:https?://|ftp://)) # Put your negative-rules here (do not match url\'s with http, https or ftp) (?:[^\\\\]|\\\\.)*? # Match anything except a backslash or backslash and a character zero or more times ungreedy (?= # Lookahead \1 # Match what was matched in group 2 \s* # Match optionally some whitespaces \) # Match ) ) ~xs'; $input = file_get_contents($css_file); preg_match_all($fontface_regex, $input, $fontfaces); // Get all font-face instances if(isset($fontfaces[0])){ // If there is a match then foreach($fontfaces[0] as $fontface){ // Foreach instance preg_match_all($url_regex, $fontface, $r); // Let's match the url's if(isset($r[0])){ // If there is a hit $results[] = $r[0]; // Then add it to the results array } } } echo '<pre>'. print_r($results, true) . '</pre>'; // Show the results
демонстрация
Присоединитесь к чату regex!