diff --git a/lib/minify/matthiasmullie-minify/src/CSS.php b/lib/minify/matthiasmullie-minify/src/CSS.php index f50d96bbf47..3ba9e551aca 100644 --- a/lib/minify/matthiasmullie-minify/src/CSS.php +++ b/lib/minify/matthiasmullie-minify/src/CSS.php @@ -44,6 +44,10 @@ class CSS extends Minify 'jpeg' => 'data:image/jpeg', 'svg' => 'data:image/svg+xml', 'woff' => 'data:application/x-font-woff', + 'woff2' => 'data:application/x-font-woff2', + 'avif' => 'data:image/avif', + 'apng' => 'data:image/apng', + 'webp' => 'data:image/webp', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'xbm' => 'image/x-xbitmap', @@ -637,35 +641,7 @@ class CSS extends Minify return $placeholder; }; - // Moodle-specific change MDL-68191 starts. - /* This was the old code: $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); - */ - // This is the new, more accurate and faster regex. - $this->registerPattern('/ - # optional newline - \n? - - # start comment - \/\* - - # comment content - (?: - # either starts with an ! - ! - | - # or, after some number of characters which do not end the comment - (?:(?!\*\/).)*? - - # there is either a @license or @preserve tag - @(?:license|preserve) - ) - - # then match to the end of the comment - .*?\*\/\n? - - /ixs', $callback); - // Moodle-specific change MDL-68191. $this->registerPattern('/\/\*.*?\*\//s', ''); } @@ -746,7 +722,7 @@ class CSS extends Minify // and since we've captured more code than required, we may have some leftover // calc() in here too - go recursive on the remaining but of code to go figure // that out and extract what is needed - $rest = str_replace($function.$expr, '', $match[0]); + $rest = $minifier->str_replace_first($function.$expr, '', $match[0]); $rest = preg_replace_callback($pattern, $callback, $rest); return $placeholder.$rest; @@ -764,7 +740,8 @@ class CSS extends Minify // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $this->registerPattern( - '/(?<=^|[;}])(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m', + + '/(?<=^|[;}{])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m', function ($match) use ($minifier) { $placeholder = '--custom-'. count($minifier->extracted) . ':0'; $minifier->extracted[$placeholder] = $match[1] .':'. trim($match[2]); diff --git a/lib/minify/matthiasmullie-minify/src/JS.php b/lib/minify/matthiasmullie-minify/src/JS.php index a0fa649d3a9..1e1679740f8 100644 --- a/lib/minify/matthiasmullie-minify/src/JS.php +++ b/lib/minify/matthiasmullie-minify/src/JS.php @@ -198,15 +198,25 @@ class JS extends Minify // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; $callback = function ($match) use ($minifier) { - $count = count($minifier->extracted); - $placeholder = '/*'.$count.'*/'; - $minifier->extracted[$placeholder] = $match[0]; + if ( + substr($match[2], 0, 1) === '!' || + strpos($match[2], '@license') !== false || + strpos($match[2], '@preserve') !== false + ) { + // preserve multi-line comments that start with /*! + // or contain @license or @preserve annotations + $count = count($minifier->extracted); + $placeholder = '/*'.$count.'*/'; + $minifier->extracted[$placeholder] = $match[0]; - return $placeholder; + return $match[1] . $placeholder . $match[3]; + } + + return $match[1] . $match[3]; }; + // multi-line comments - $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); - $this->registerPattern('/\/\*.*?\*\//s', ''); + $this->registerPattern('/(\n?)\/\*(.*?)\*\/(\n?)/s', $callback); // single-line comments $this->registerPattern('/\/\/.*$/m', ''); @@ -254,7 +264,7 @@ class JS extends Minify // of the RegExp methods (a `\` followed by a variable or value is // likely part of a division, not a regex) $keywords = array('do', 'in', 'new', 'else', 'throw', 'yield', 'delete', 'return', 'typeof'); - $before = '(^|[=:,;\+\-\*\/\}\(\{\[&\|!]|'.implode('|', $keywords).')\s*'; + $before = '(^|[=:,;\+\-\*\?\/\}\(\{\[&\|!]|'.implode('|', $keywords).')\s*'; $propertiesAndMethods = array( // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Properties_2 'constructor', @@ -274,7 +284,7 @@ class JS extends Minify ); $delimiters = array_fill(0, count($propertiesAndMethods), '/'); $propertiesAndMethods = array_map('preg_quote', $propertiesAndMethods, $delimiters); - $after = '(?=\s*([\.,;\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))'; + $after = '(?=\s*([\.,;:\)\}&\|+]|\/\/|$|\.('.implode('|', $propertiesAndMethods).')))'; $this->registerPattern('/'.$before.'\K'.$pattern.$after.'/', $callback); // regular expressions following a `)` are rather annoying to detect... @@ -405,9 +415,26 @@ class JS extends Minify * to be the for-loop's body... Same goes for while loops. * I'm going to double that semicolon (if any) so after the next line, * which strips semicolons here & there, we're still left with this one. + * Note the special recursive construct in the three inner parts of the for: + * (\{([^\{\}]*(?-2))*[^\{\}]*\})? - it is intended to match inline + * functions bodies, e.g.: ipatterns), -1); $matches = array(); - while ($content) { + while ($processedOffset < $contentLength) { // find first match for all patterns foreach ($this->patterns as $i => $pattern) { list($pattern, $replacement) = $pattern; @@ -285,12 +290,12 @@ abstract class Minify // no need to re-run matches that are still in the part of the // content that hasn't been processed - if ($positions[$i] >= 0) { + if ($positions[$i] >= $processedOffset) { continue; } $match = null; - if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE)) { + if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { $matches[$i] = $match; // we'll store the match position as well; that way, we @@ -307,61 +312,52 @@ abstract class Minify // no more matches to find: everything's been processed, break out if (!$matches) { - $processed .= $content; + // output the remaining content + $output .= substr($content, $processedOffset); break; } // see which of the patterns actually found the first thing (we'll // only want to execute that one, since we're unsure if what the // other found was not inside what the first found) - $discardLength = min($positions); - $firstPattern = array_search($discardLength, $positions); - $match = $matches[$firstPattern][0][0]; + $matchOffset = min($positions); + $firstPattern = array_search($matchOffset, $positions); + $match = $matches[$firstPattern]; // execute the pattern that matches earliest in the content string - list($pattern, $replacement) = $this->patterns[$firstPattern]; - $replacement = $this->replacePattern($pattern, $replacement, $content); + list(, $replacement) = $this->patterns[$firstPattern]; - // figure out which part of the string was unmatched; that's the - // part we'll execute the patterns on again next - $content = (string) substr($content, $discardLength); - $unmatched = (string) substr($content, strpos($content, $match) + strlen($match)); - - // move the replaced part to $processed and prepare $content to - // again match batch of patterns against - $processed .= substr($replacement, 0, strlen($replacement) - strlen($unmatched)); - $content = $unmatched; - - // first match has been replaced & that content is to be left alone, - // the next matches will start after this replacement, so we should - // fix their offsets - foreach ($positions as $i => $position) { - $positions[$i] -= $discardLength + strlen($match); - } + // add the part of the input between $processedOffset and the first match; + // that content wasn't matched by anything + $output .= substr($content, $processedOffset, $matchOffset - $processedOffset); + // add the replacement for the match + $output .= $this->executeReplacement($replacement, $match); + // advance $processedOffset past the match + $processedOffset = $matchOffset + strlen($match[0][0]); } - return $processed; + return $output; } /** - * This is where a pattern is matched against $content and the matches - * are replaced by their respective value. - * This function will be called plenty of times, where $content will always - * move up 1 character. + * If $replacement is a callback, execute it, passing in the match data. + * If it's a string, just pass it through. * - * @param string $pattern Pattern to match * @param string|callable $replacement Replacement value - * @param string $content Content to match pattern against + * @param array $match Match data, in PREG_OFFSET_CAPTURE form * * @return string */ - protected function replacePattern($pattern, $replacement, $content) + protected function executeReplacement($replacement, $match) { - if (is_callable($replacement)) { - return preg_replace_callback($pattern, $replacement, $content, 1, $count); - } else { - return preg_replace($pattern, $replacement, $content, 1, $count); + if (!is_callable($replacement)) { + return $replacement; } + // convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects + foreach ($match as &$matchItem) { + $matchItem = $matchItem[0]; + } + return $replacement($match); } /** @@ -498,4 +494,12 @@ abstract class Minify throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); } } + + protected static function str_replace_first($search, $replace, $subject) { + $pos = strpos($subject, $search); + if ($pos !== false) { + return substr_replace($subject, $replace, $pos, strlen($search)); + } + return $subject; + } }