mikuli.cz

:)
git clone https://git.sr.ht/~ashymad/mikuli.cz
Log | Files | Refs

commit 6f805cb47991776764b5f9c1e0b3907391ad64de
parent b9778aa607797c590167cc263fd5c3cfa039499a
Author: markseu <mark2011@mayberg.se>
Date:   Tue, 10 May 2022 16:46:26 +0200

Updated bundle extension, latest library

Diffstat:
Msystem/extensions/bundle.php | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msystem/extensions/update-current.ini | 4++--
2 files changed, 168 insertions(+), 65 deletions(-)

diff --git a/system/extensions/bundle.php b/system/extensions/bundle.php @@ -2,7 +2,7 @@ // Bundle extension, https://github.com/datenstrom/yellow-extensions/tree/master/source/bundle class YellowBundle { - const VERSION = "0.8.26"; + const VERSION = "0.8.27"; public $yellow; // access to API // Handle initialisation @@ -281,6 +281,44 @@ abstract class Minify } /** + * Add a file to be minified. + * + * @param string|string[] $data + * + * @return static + * + * @throws IOException + */ + public function addFile($data /* $data = null, ... */) + { + // bogus "usage" of parameter $data: scrutinizer warns this variable is + // not used (we're using func_get_args instead to support overloading), + // but it still needs to be defined because it makes no sense to have + // this function without argument :) + $args = array($data) + func_get_args(); + + // this method can be overloaded + foreach ($args as $path) { + if (is_array($path)) { + call_user_func_array(array($this, 'addFile'), $path); + continue; + } + + // redefine var + $path = (string) $path; + + // check if we can read the file + if (!$this->canImportFile($path)) { + throw new IOException('The file "'.$path.'" could not be opened for reading. Check if PHP has enough permissions.'); + } + + $this->add($path); + } + + return $this; + } + + /** * Minify the data & (optionally) saves it to a file. * * @param string[optional] $path Path to write the data to @@ -386,6 +424,9 @@ abstract class Minify /** * Register a pattern to execute against the source content. * + * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work. + * If you need that functionality, use a callback instead. + * * @param string $pattern PCRE pattern * @param string|callable $replacement Replacement value for matched pattern */ @@ -411,11 +452,13 @@ abstract class Minify */ protected function replace($content) { - $processed = ''; + $contentLength = strlen($content); + $output = ''; + $processedOffset = 0; $positions = array_fill(0, count($this->patterns), -1); $matches = array(); - while ($content) { + while ($processedOffset < $contentLength) { // find first match for all patterns foreach ($this->patterns as $i => $pattern) { list($pattern, $replacement) = $pattern; @@ -428,12 +471,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 @@ -450,61 +493,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); - - // 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); - } + list(, $replacement) = $this->patterns[$firstPattern]; + + // 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); } /** @@ -615,7 +649,7 @@ abstract class Minify */ protected function openFileForWriting($path) { - if (($handler = @fopen($path, 'w')) === false) { + if ($path === '' || ($handler = @fopen($path, 'w')) === false) { throw new IOException('The file "'.$path.'" could not be opened for writing. Check if PHP has enough permissions.'); } @@ -633,7 +667,11 @@ abstract class Minify */ protected function writeToFile($handler, $content, $path = '') { - if (($result = @fwrite($handler, $content)) === false || ($result < strlen($content))) { + if ( + !is_resource($handler) || + ($result = @fwrite($handler, $content)) === false || + ($result < strlen($content)) + ) { throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); } } @@ -657,6 +695,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', @@ -829,7 +871,7 @@ class CSS extends Minify // grab referenced file & minify it (which may include importing // yet other @import statements recursively) - $minifier = new static($importPath); + $minifier = new self($importPath); $minifier->setMaxImportSize($this->maxImportSize); $minifier->setImportExtensions($this->importExtensions); $importContent = $minifier->execute($source, $parents); @@ -920,7 +962,8 @@ class CSS extends Minify */ $this->extractStrings(); $this->stripComments(); - $this->extractCalcs(); + $this->extractMath(); + $this->extractCustomProperties(); $css = $this->replace($css); $css = $this->stripWhitespace($css); @@ -1291,19 +1334,29 @@ class CSS extends Minify } /** - * Replace all `calc()` occurrences. + * Replace all occurrences of functions that may contain math, where + * whitespace around operators needs to be preserved (e.g. calc, clamp) */ - protected function extractCalcs() + protected function extractMath() { + $functions = array('calc', 'clamp', 'min', 'max'); + $pattern = '/\b('. implode('|', $functions) .')(\(.+?)(?=$|;|})/m'; + // PHP only supports $this inside anonymous functions since 5.4 $minifier = $this; - $callback = function ($match) use ($minifier) { - $length = strlen($match[1]); + $callback = function ($match) use ($minifier, $pattern, &$callback) { + $function = $match[1]; + $length = strlen($match[2]); $expr = ''; $opened = 0; + // the regular expression for extracting math has 1 significant problem: + // it can't determine the correct closing parenthesis... + // instead, it'll match a larger portion of code to where it's certain that + // the calc() musts have ended, and we'll figure out which is the correct + // closing parenthesis here, by counting how many have opened for ($i = 0; $i < $length; $i++) { - $char = $match[1][$i]; + $char = $match[2][$i]; $expr .= $char; if ($char === '(') { $opened++; @@ -1311,18 +1364,41 @@ class CSS extends Minify break; } } - $rest = str_replace($expr, '', $match[1]); - $expr = trim(substr($expr, 1, -1)); + // now that we've figured out where the calc() starts and ends, extract it $count = count($minifier->extracted); - $placeholder = 'calc('.$count.')'; - $minifier->extracted[$placeholder] = 'calc('.$expr.')'; + $placeholder = 'math('.$count.')'; + $minifier->extracted[$placeholder] = $function.'('.trim(substr($expr, 1, -1)).')'; + + // 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 = preg_replace_callback($pattern, $callback, $rest); return $placeholder.$rest; }; - $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/', $callback); - $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/m', $callback); + $this->registerPattern($pattern, $callback); + } + + /** + * Replace custom properties, whose values may be used in scenarios where + * we wouldn't want them to be minified (e.g. inside calc) + */ + protected function extractCustomProperties() + { + // PHP only supports $this inside anonymous functions since 5.4 + $minifier = $this; + $this->registerPattern( + '/(?<=^|[;}])\s*(--[^:;{}"\'\s]+)\s*:([^;{}]+)/m', + function ($match) use ($minifier) { + $placeholder = '--custom-'. count($minifier->extracted) . ':0'; + $minifier->extracted[$placeholder] = $match[1] .':'. trim($match[2]); + return $placeholder; + + } + ); } /** @@ -1541,15 +1617,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 $match[1] . $placeholder . $match[3]; + } - return $placeholder; + 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', ''); @@ -1597,7 +1683,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', @@ -1748,9 +1834,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.: i<arr.map(function(e){return e}).length. + * Also note that the construct is applied only once and multiplied + * for each part of the for, otherwise it risks a catastrophic backtracking. + * The limitation is that it will not allow closures in more than one + * of the three parts for a specific for() case. + * REGEX throwing catastrophic backtracking: $content = preg_replace('/(for\([^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*;[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*;[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*\));(\}|$)/s', '\\1;;\\8', $content); */ - $content = preg_replace('/(for\([^;\{]*;[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\2', $content); + $content = preg_replace('/(for\((?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*);[^;\{]*;[^;\{]*\));(\}|$)/s', '\\1;;\\4', $content); + $content = preg_replace('/(for\([^;\{]*;(?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*);[^;\{]*\));(\}|$)/s', '\\1;;\\4', $content); + $content = preg_replace('/(for\([^;\{]*;[^;\{]*;(?:[^;\{]*|[^;\{]*function[^;\{]*(\{([^\{\}]*(?-2))*[^\{\}]*\})?[^;\{]*)\));(\}|$)/s', '\\1;;\\4', $content); + $content = preg_replace('/(for\([^;\{]+\s+in\s+[^;\{]+\));(\}|$)/s', '\\1;;\\2', $content); + + /* + * Do the same for the if's that don't have a body but are followed by ;} + */ + $content = preg_replace('/(\bif\s*\([^{;]*\));\}/s', '\\1;;}', $content); + /* * Below will also keep `;` after a `do{}while();` along with `while();` * While these could be stripped after do-while, detecting this diff --git a/system/extensions/update-current.ini b/system/extensions/update-current.ini @@ -1,11 +1,11 @@ # Datenstrom Yellow update settings Extension: Bundle -Version: 0.8.26 +Version: 0.8.27 Description: Bundle website files. DocumentationUrl: https://github.com/datenstrom/yellow-extensions/tree/master/source/bundle DownloadUrl: https://github.com/datenstrom/yellow-extensions/raw/master/zip/bundle.zip -Published: 2022-04-18 17:44:11 +Published: 2022-05-10 16:37:18 Developer: Datenstrom Tag: feature system/extensions/bundle.php: bundle.php, create, update