Извлечение URL-адресов из @ font-face путем поиска в @ font-face для замены

У меня есть веб-служба, которая перезаписывает 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 () .
Давайте использовать некоторое регулярное выражение Фу !!!

Часть 1: сопоставление URL-адресов, которые не находятся в пределах @ font-face {}

1.1 Соответствие @ font-face {}

О да, это может показаться «странным», но позже вы заметите, почему 🙂
Нам понадобится рекурсивное регулярное выражение:

 @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 

демонстрация

1.2 Escaping @ font-face {}

Мы будем использовать (*SKIP)(*FAIL) сразу после предыдущего регулярного выражения, он пропустит его. См. Этот ответ, чтобы понять, как это работает.

демонстрация

1.3 Соответствие URL ()

Мы будем использовать что-то вроде этого:

 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 .

демонстрация

1.4 Соответствие значения внутри url ()

Возможно, вы догадались, что нам нужно использовать некоторый 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 ) ) 

демонстрация

1.5 Использование шаблона в PHP

Нам нужно избежать некоторых вещей, таких как кавычки, обратные \\\\ = \ , использовать правильную функцию и правые модификаторы:

 $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 {}

2.1. Другой подход

Я хочу сделать эту часть в 2 регулярных выражениях, потому что будет больно сопоставлять URL-адреса, которые находятся внутри @font-face {} то же время заботясь о состоянии фигурных скобок {} в рекурсивном регулярном выражении.

И поскольку у нас уже есть нужные нам фрагменты, нам нужно будет их применять только в некотором коде:

  1. Сопоставьте все экземпляры @font-face {}
  2. Прокрутите их и сопоставьте все url ()

2.2 Ввод его в код

 $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!