Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 /** 3 * CSS Minifier 4 * 5 * Please report bugs on https://github.com/matthiasmullie/minify/issues 6 * 7 * @author Matthias Mullie <minify@mullie.eu> 8 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved 9 * @license MIT License 10 */ 11 12 namespace MatthiasMullie\Minify; 13 14 use MatthiasMullie\Minify\Exceptions\FileImportException; 15 use MatthiasMullie\PathConverter\ConverterInterface; 16 use MatthiasMullie\PathConverter\Converter; 17 18 /** 19 * CSS minifier 20 * 21 * Please report bugs on https://github.com/matthiasmullie/minify/issues 22 * 23 * @package Minify 24 * @author Matthias Mullie <minify@mullie.eu> 25 * @author Tijs Verkoyen <minify@verkoyen.eu> 26 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved 27 * @license MIT License 28 */ 29 class CSS extends Minify 30 { 31 /** 32 * @var int maximum inport size in kB 33 */ 34 protected $maxImportSize = 5; 35 36 /** 37 * @var string[] valid import extensions 38 */ 39 protected $importExtensions = array( 40 'gif' => 'data:image/gif', 41 'png' => 'data:image/png', 42 'jpe' => 'data:image/jpeg', 43 'jpg' => 'data:image/jpeg', 44 'jpeg' => 'data:image/jpeg', 45 'svg' => 'data:image/svg+xml', 46 'woff' => 'data:application/x-font-woff', 47 'tif' => 'image/tiff', 48 'tiff' => 'image/tiff', 49 'xbm' => 'image/x-xbitmap', 50 ); 51 52 /** 53 * Set the maximum size if files to be imported. 54 * 55 * Files larger than this size (in kB) will not be imported into the CSS. 56 * Importing files into the CSS as data-uri will save you some connections, 57 * but we should only import relatively small decorative images so that our 58 * CSS file doesn't get too bulky. 59 * 60 * @param int $size Size in kB 61 */ 62 public function setMaxImportSize($size) 63 { 64 $this->maxImportSize = $size; 65 } 66 67 /** 68 * Set the type of extensions to be imported into the CSS (to save network 69 * connections). 70 * Keys of the array should be the file extensions & respective values 71 * should be the data type. 72 * 73 * @param string[] $extensions Array of file extensions 74 */ 75 public function setImportExtensions(array $extensions) 76 { 77 $this->importExtensions = $extensions; 78 } 79 80 /** 81 * Move any import statements to the top. 82 * 83 * @param string $content Nearly finished CSS content 84 * 85 * @return string 86 */ 87 protected function moveImportsToTop($content) 88 { 89 if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) { 90 // remove from content 91 foreach ($matches[0] as $import) { 92 $content = str_replace($import, '', $content); 93 } 94 95 // add to top 96 $content = implode(';', $matches[2]).';'.trim($content, ';'); 97 } 98 99 return $content; 100 } 101 102 /** 103 * Combine CSS from import statements. 104 * 105 * @import's will be loaded and their content merged into the original file, 106 * to save HTTP requests. 107 * 108 * @param string $source The file to combine imports for 109 * @param string $content The CSS content to combine imports for 110 * @param string[] $parents Parent paths, for circular reference checks 111 * 112 * @return string 113 * 114 * @throws FileImportException 115 */ 116 protected function combineImports($source, $content, $parents) 117 { 118 $importRegexes = array( 119 // @import url(xxx) 120 '/ 121 # import statement 122 @import 123 124 # whitespace 125 \s+ 126 127 # open url() 128 url\( 129 130 # (optional) open path enclosure 131 (?P<quotes>["\']?) 132 133 # fetch path 134 (?P<path>.+?) 135 136 # (optional) close path enclosure 137 (?P=quotes) 138 139 # close url() 140 \) 141 142 # (optional) trailing whitespace 143 \s* 144 145 # (optional) media statement(s) 146 (?P<media>[^;]*) 147 148 # (optional) trailing whitespace 149 \s* 150 151 # (optional) closing semi-colon 152 ;? 153 154 /ix', 155 156 // @import 'xxx' 157 '/ 158 159 # import statement 160 @import 161 162 # whitespace 163 \s+ 164 165 # open path enclosure 166 (?P<quotes>["\']) 167 168 # fetch path 169 (?P<path>.+?) 170 171 # close path enclosure 172 (?P=quotes) 173 174 # (optional) trailing whitespace 175 \s* 176 177 # (optional) media statement(s) 178 (?P<media>[^;]*) 179 180 # (optional) trailing whitespace 181 \s* 182 183 # (optional) closing semi-colon 184 ;? 185 186 /ix', 187 ); 188 189 // find all relative imports in css 190 $matches = array(); 191 foreach ($importRegexes as $importRegex) { 192 if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) { 193 $matches = array_merge($matches, $regexMatches); 194 } 195 } 196 197 $search = array(); 198 $replace = array(); 199 200 // loop the matches 201 foreach ($matches as $match) { 202 // get the path for the file that will be imported 203 $importPath = dirname($source).'/'.$match['path']; 204 205 // only replace the import with the content if we can grab the 206 // content of the file 207 if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) { 208 continue; 209 } 210 211 // check if current file was not imported previously in the same 212 // import chain. 213 if (in_array($importPath, $parents)) { 214 throw new FileImportException('Failed to import file "'.$importPath.'": circular reference detected.'); 215 } 216 217 // grab referenced file & minify it (which may include importing 218 // yet other @import statements recursively) 219 $minifier = new static($importPath); 220 $minifier->setMaxImportSize($this->maxImportSize); 221 $minifier->setImportExtensions($this->importExtensions); 222 $importContent = $minifier->execute($source, $parents); 223 224 // check if this is only valid for certain media 225 if (!empty($match['media'])) { 226 $importContent = '@media '.$match['media'].'{'.$importContent.'}'; 227 } 228 229 // add to replacement array 230 $search[] = $match[0]; 231 $replace[] = $importContent; 232 } 233 234 // replace the import statements 235 return str_replace($search, $replace, $content); 236 } 237 238 /** 239 * Import files into the CSS, base64-ized. 240 * 241 * @url(image.jpg) images will be loaded and their content merged into the 242 * original file, to save HTTP requests. 243 * 244 * @param string $source The file to import files for 245 * @param string $content The CSS content to import files for 246 * 247 * @return string 248 */ 249 protected function importFiles($source, $content) 250 { 251 $regex = '/url\((["\']?)(.+?)\\1\)/i'; 252 if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) { 253 $search = array(); 254 $replace = array(); 255 256 // loop the matches 257 foreach ($matches as $match) { 258 $extension = substr(strrchr($match[2], '.'), 1); 259 if ($extension && !array_key_exists($extension, $this->importExtensions)) { 260 continue; 261 } 262 263 // get the path for the file that will be imported 264 $path = $match[2]; 265 $path = dirname($source).'/'.$path; 266 267 // only replace the import with the content if we're able to get 268 // the content of the file, and it's relatively small 269 if ($this->canImportFile($path) && $this->canImportBySize($path)) { 270 // grab content && base64-ize 271 $importContent = $this->load($path); 272 $importContent = base64_encode($importContent); 273 274 // build replacement 275 $search[] = $match[0]; 276 $replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')'; 277 } 278 } 279 280 // replace the import statements 281 $content = str_replace($search, $replace, $content); 282 } 283 284 return $content; 285 } 286 287 /** 288 * Minify the data. 289 * Perform CSS optimizations. 290 * 291 * @param string[optional] $path Path to write the data to 292 * @param string[] $parents Parent paths, for circular reference checks 293 * 294 * @return string The minified data 295 */ 296 public function execute($path = null, $parents = array()) 297 { 298 $content = ''; 299 300 // loop CSS data (raw data and files) 301 foreach ($this->data as $source => $css) { 302 /* 303 * Let's first take out strings & comments, since we can't just 304 * remove whitespace anywhere. If whitespace occurs inside a string, 305 * we should leave it alone. E.g.: 306 * p { content: "a test" } 307 */ 308 $this->extractStrings(); 309 $this->stripComments(); 310 $this->extractCalcs(); 311 $css = $this->replace($css); 312 313 $css = $this->stripWhitespace($css); 314 $css = $this->shortenColors($css); 315 $css = $this->shortenZeroes($css); 316 $css = $this->shortenFontWeights($css); 317 $css = $this->stripEmptyTags($css); 318 319 // restore the string we've extracted earlier 320 $css = $this->restoreExtractedData($css); 321 322 $source = is_int($source) ? '' : $source; 323 $parents = $source ? array_merge($parents, array($source)) : $parents; 324 $css = $this->combineImports($source, $css, $parents); 325 $css = $this->importFiles($source, $css); 326 327 /* 328 * If we'll save to a new path, we'll have to fix the relative paths 329 * to be relative no longer to the source file, but to the new path. 330 * If we don't write to a file, fall back to same path so no 331 * conversion happens (because we still want it to go through most 332 * of the move code, which also addresses url() & @import syntax...) 333 */ 334 $converter = $this->getPathConverter($source, $path ?: $source); 335 $css = $this->move($converter, $css); 336 337 // combine css 338 $content .= $css; 339 } 340 341 $content = $this->moveImportsToTop($content); 342 343 return $content; 344 } 345 346 /** 347 * Moving a css file should update all relative urls. 348 * Relative references (e.g. ../images/image.gif) in a certain css file, 349 * will have to be updated when a file is being saved at another location 350 * (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper). 351 * 352 * @param ConverterInterface $converter Relative path converter 353 * @param string $content The CSS content to update relative urls for 354 * 355 * @return string 356 */ 357 protected function move(ConverterInterface $converter, $content) 358 { 359 /* 360 * Relative path references will usually be enclosed by url(). @import 361 * is an exception, where url() is not necessary around the path (but is 362 * allowed). 363 * This *could* be 1 regular expression, where both regular expressions 364 * in this array are on different sides of a |. But we're using named 365 * patterns in both regexes, the same name on both regexes. This is only 366 * possible with a (?J) modifier, but that only works after a fairly 367 * recent PCRE version. That's why I'm doing 2 separate regular 368 * expressions & combining the matches after executing of both. 369 */ 370 $relativeRegexes = array( 371 // url(xxx) 372 '/ 373 # open url() 374 url\( 375 376 \s* 377 378 # open path enclosure 379 (?P<quotes>["\'])? 380 381 # fetch path 382 (?P<path>.+?) 383 384 # close path enclosure 385 (?(quotes)(?P=quotes)) 386 387 \s* 388 389 # close url() 390 \) 391 392 /ix', 393 394 // @import "xxx" 395 '/ 396 # import statement 397 @import 398 399 # whitespace 400 \s+ 401 402 # we don\'t have to check for @import url(), because the 403 # condition above will already catch these 404 405 # open path enclosure 406 (?P<quotes>["\']) 407 408 # fetch path 409 (?P<path>.+?) 410 411 # close path enclosure 412 (?P=quotes) 413 414 /ix', 415 ); 416 417 // find all relative urls in css 418 $matches = array(); 419 foreach ($relativeRegexes as $relativeRegex) { 420 if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) { 421 $matches = array_merge($matches, $regexMatches); 422 } 423 } 424 425 $search = array(); 426 $replace = array(); 427 428 // loop all urls 429 foreach ($matches as $match) { 430 // determine if it's a url() or an @import match 431 $type = (strpos($match[0], '@import') === 0 ? 'import' : 'url'); 432 433 $url = $match['path']; 434 if ($this->canImportByPath($url)) { 435 // attempting to interpret GET-params makes no sense, so let's discard them for awhile 436 $params = strrchr($url, '?'); 437 $url = $params ? substr($url, 0, -strlen($params)) : $url; 438 439 // fix relative url 440 $url = $converter->convert($url); 441 442 // now that the path has been converted, re-apply GET-params 443 $url .= $params; 444 } 445 446 /* 447 * Urls with control characters above 0x7e should be quoted. 448 * According to Mozilla's parser, whitespace is only allowed at the 449 * end of unquoted urls. 450 * Urls with `)` (as could happen with data: uris) should also be 451 * quoted to avoid being confused for the url() closing parentheses. 452 * And urls with a # have also been reported to cause issues. 453 * Urls with quotes inside should also remain escaped. 454 * 455 * @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation 456 * @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378 457 * @see https://github.com/matthiasmullie/minify/issues/193 458 */ 459 $url = trim($url); 460 if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) { 461 $url = $match['quotes'] . $url . $match['quotes']; 462 } 463 464 // build replacement 465 $search[] = $match[0]; 466 if ($type === 'url') { 467 $replace[] = 'url('.$url.')'; 468 } elseif ($type === 'import') { 469 $replace[] = '@import "'.$url.'"'; 470 } 471 } 472 473 // replace urls 474 return str_replace($search, $replace, $content); 475 } 476 477 /** 478 * Shorthand hex color codes. 479 * #FF0000 -> #F00. 480 * 481 * @param string $content The CSS content to shorten the hex color codes for 482 * 483 * @return string 484 */ 485 protected function shortenColors($content) 486 { 487 $content = preg_replace('/(?<=[: ])#([0-9a-z])\\1([0-9a-z])\\2([0-9a-z])\\3(?:([0-9a-z])\\4)?(?=[; }])/i', '#$1$2$3$4', $content); 488 489 // remove alpha channel if it's pointless... 490 $content = preg_replace('/(?<=[: ])#([0-9a-z]{6})ff?(?=[; }])/i', '#$1', $content); 491 $content = preg_replace('/(?<=[: ])#([0-9a-z]{3})f?(?=[; }])/i', '#$1', $content); 492 493 $colors = array( 494 // we can shorten some even more by replacing them with their color name 495 '#F0FFFF' => 'azure', 496 '#F5F5DC' => 'beige', 497 '#A52A2A' => 'brown', 498 '#FF7F50' => 'coral', 499 '#FFD700' => 'gold', 500 '#808080' => 'gray', 501 '#008000' => 'green', 502 '#4B0082' => 'indigo', 503 '#FFFFF0' => 'ivory', 504 '#F0E68C' => 'khaki', 505 '#FAF0E6' => 'linen', 506 '#800000' => 'maroon', 507 '#000080' => 'navy', 508 '#808000' => 'olive', 509 '#CD853F' => 'peru', 510 '#FFC0CB' => 'pink', 511 '#DDA0DD' => 'plum', 512 '#800080' => 'purple', 513 '#F00' => 'red', 514 '#FA8072' => 'salmon', 515 '#A0522D' => 'sienna', 516 '#C0C0C0' => 'silver', 517 '#FFFAFA' => 'snow', 518 '#D2B48C' => 'tan', 519 '#FF6347' => 'tomato', 520 '#EE82EE' => 'violet', 521 '#F5DEB3' => 'wheat', 522 // or the other way around 523 'WHITE' => '#fff', 524 'BLACK' => '#000', 525 ); 526 527 return preg_replace_callback( 528 '/(?<=[: ])('.implode('|', array_keys($colors)).')(?=[; }])/i', 529 function ($match) use ($colors) { 530 return $colors[strtoupper($match[0])]; 531 }, 532 $content 533 ); 534 } 535 536 /** 537 * Shorten CSS font weights. 538 * 539 * @param string $content The CSS content to shorten the font weights for 540 * 541 * @return string 542 */ 543 protected function shortenFontWeights($content) 544 { 545 $weights = array( 546 'normal' => 400, 547 'bold' => 700, 548 ); 549 550 $callback = function ($match) use ($weights) { 551 return $match[1].$weights[$match[2]]; 552 }; 553 554 return preg_replace_callback('/(font-weight\s*:\s*)('.implode('|', array_keys($weights)).')(?=[;}])/', $callback, $content); 555 } 556 557 /** 558 * Shorthand 0 values to plain 0, instead of e.g. -0em. 559 * 560 * @param string $content The CSS content to shorten the zero values for 561 * 562 * @return string 563 */ 564 protected function shortenZeroes($content) 565 { 566 // we don't want to strip units in `calc()` expressions: 567 // `5px - 0px` is valid, but `5px - 0` is not 568 // `10px * 0` is valid (equates to 0), and so is `10 * 0px`, but 569 // `10 * 0` is invalid 570 // we've extracted calcs earlier, so we don't need to worry about this 571 572 // reusable bits of code throughout these regexes: 573 // before & after are used to make sure we don't match lose unintended 574 // 0-like values (e.g. in #000, or in http://url/1.0) 575 // units can be stripped from 0 values, or used to recognize non 0 576 // values (where wa may be able to strip a .0 suffix) 577 $before = '(?<=[:(, ])'; 578 $after = '(?=[ ,);}])'; 579 $units = '(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)'; 580 581 // strip units after zeroes (0px -> 0) 582 // NOTE: it should be safe to remove all units for a 0 value, but in 583 // practice, Webkit (especially Safari) seems to stumble over at least 584 // 0%, potentially other units as well. Only stripping 'px' for now. 585 // @see https://github.com/matthiasmullie/minify/issues/60 586 $content = preg_replace('/'.$before.'(-?0*(\.0+)?)(?<=0)px'.$after.'/', '\\1', $content); 587 588 // strip 0-digits (.0 -> 0) 589 $content = preg_replace('/'.$before.'\.0+'.$units.'?'.$after.'/', '0\\1', $content); 590 // strip trailing 0: 50.10 -> 50.1, 50.10px -> 50.1px 591 $content = preg_replace('/'.$before.'(-?[0-9]+\.[0-9]+)0+'.$units.'?'.$after.'/', '\\1\\2', $content); 592 // strip trailing 0: 50.00 -> 50, 50.00px -> 50px 593 $content = preg_replace('/'.$before.'(-?[0-9]+)\.0+'.$units.'?'.$after.'/', '\\1\\2', $content); 594 // strip leading 0: 0.1 -> .1, 01.1 -> 1.1 595 $content = preg_replace('/'.$before.'(-?)0+([0-9]*\.[0-9]+)'.$units.'?'.$after.'/', '\\1\\2\\3', $content); 596 597 // strip negative zeroes (-0 -> 0) & truncate zeroes (00 -> 0) 598 $content = preg_replace('/'.$before.'-?0+'.$units.'?'.$after.'/', '0\\1', $content); 599 600 // IE doesn't seem to understand a unitless flex-basis value (correct - 601 // it goes against the spec), so let's add it in again (make it `%`, 602 // which is only 1 char: 0%, 0px, 0 anything, it's all just the same) 603 // @see https://developer.mozilla.org/nl/docs/Web/CSS/flex 604 $content = preg_replace('/flex:([0-9]+\s[0-9]+\s)0([;\}])/', 'flex:$1}0%$2}', $content); 605 $content = preg_replace('/flex-basis:0([;\}])/', 'flex-basis:0%$1}', $content); 606 607 return $content; 608 } 609 610 /** 611 * Strip empty tags from source code. 612 * 613 * @param string $content 614 * 615 * @return string 616 */ 617 protected function stripEmptyTags($content) 618 { 619 $content = preg_replace('/(?<=^)[^\{\};]+\{\s*\}/', '', $content); 620 $content = preg_replace('/(?<=(\}|;))[^\{\};]+\{\s*\}/', '', $content); 621 622 return $content; 623 } 624 625 /** 626 * Strip comments from source code. 627 */ 628 protected function stripComments() 629 { 630 // PHP only supports $this inside anonymous functions since 5.4 631 $minifier = $this; 632 $callback = function ($match) use ($minifier) { 633 $count = count($minifier->extracted); 634 $placeholder = '/*'.$count.'*/'; 635 $minifier->extracted[$placeholder] = $match[0]; 636 637 return $placeholder; 638 }; 639 // Moodle-specific change MDL-68191 starts. 640 /* This was the old code: 641 $this->registerPattern('/\n?\/\*(!|.*?@license|.*?@preserve).*?\*\/\n?/s', $callback); 642 */ 643 // This is the new, more accurate and faster regex. 644 $this->registerPattern('/ 645 # optional newline 646 \n? 647 648 # start comment 649 \/\* 650 651 # comment content 652 (?: 653 # either starts with an ! 654 ! 655 | 656 # or, after some number of characters which do not end the comment 657 (?:(?!\*\/).)*? 658 659 # there is either a @license or @preserve tag 660 @(?:license|preserve) 661 ) 662 663 # then match to the end of the comment 664 .*?\*\/\n? 665 666 /ixs', $callback); 667 // Moodle-specific change MDL-68191. 668 669 $this->registerPattern('/\/\*.*?\*\//s', ''); 670 } 671 672 /** 673 * Strip whitespace. 674 * 675 * @param string $content The CSS content to strip the whitespace for 676 * 677 * @return string 678 */ 679 protected function stripWhitespace($content) 680 { 681 // remove leading & trailing whitespace 682 $content = preg_replace('/^\s*/m', '', $content); 683 $content = preg_replace('/\s*$/m', '', $content); 684 685 // replace newlines with a single space 686 $content = preg_replace('/\s+/', ' ', $content); 687 688 // remove whitespace around meta characters 689 // inspired by stackoverflow.com/questions/15195750/minify-compress-css-with-regex 690 $content = preg_replace('/\s*([\*$~^|]?+=|[{};,>~]|!important\b)\s*/', '$1', $content); 691 $content = preg_replace('/([\[(:>\+])\s+/', '$1', $content); 692 $content = preg_replace('/\s+([\]\)>\+])/', '$1', $content); 693 $content = preg_replace('/\s+(:)(?![^\}]*\{)/', '$1', $content); 694 695 // whitespace around + and - can only be stripped inside some pseudo- 696 // classes, like `:nth-child(3+2n)` 697 // not in things like `calc(3px + 2px)`, shorthands like `3px -2px`, or 698 // selectors like `div.weird- p` 699 $pseudos = array('nth-child', 'nth-last-child', 'nth-last-of-type', 'nth-of-type'); 700 $content = preg_replace('/:('.implode('|', $pseudos).')\(\s*([+-]?)\s*(.+?)\s*([+-]?)\s*(.*?)\s*\)/', ':$1($2$3$4$5)', $content); 701 702 // remove semicolon/whitespace followed by closing bracket 703 $content = str_replace(';}', '}', $content); 704 705 return trim($content); 706 } 707 708 /** 709 * Replace all `calc()` occurrences. 710 */ 711 protected function extractCalcs() 712 { 713 // PHP only supports $this inside anonymous functions since 5.4 714 $minifier = $this; 715 $callback = function ($match) use ($minifier) { 716 $length = strlen($match[1]); 717 $expr = ''; 718 $opened = 0; 719 720 for ($i = 0; $i < $length; $i++) { 721 $char = $match[1][$i]; 722 $expr .= $char; 723 if ($char === '(') { 724 $opened++; 725 } elseif ($char === ')' && --$opened === 0) { 726 break; 727 } 728 } 729 $rest = str_replace($expr, '', $match[1]); 730 $expr = trim(substr($expr, 1, -1)); 731 732 $count = count($minifier->extracted); 733 $placeholder = 'calc('.$count.')'; 734 $minifier->extracted[$placeholder] = 'calc('.$expr.')'; 735 736 return $placeholder.$rest; 737 }; 738 739 $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/', $callback); 740 $this->registerPattern('/calc(\(.+?)(?=$|;|}|calc\()/m', $callback); 741 } 742 743 /** 744 * Check if file is small enough to be imported. 745 * 746 * @param string $path The path to the file 747 * 748 * @return bool 749 */ 750 protected function canImportBySize($path) 751 { 752 return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024; 753 } 754 755 /** 756 * Check if file a file can be imported, going by the path. 757 * 758 * @param string $path 759 * 760 * @return bool 761 */ 762 protected function canImportByPath($path) 763 { 764 return preg_match('/^(data:|https?:|\\/)/', $path) === 0; 765 } 766 767 /** 768 * Return a converter to update relative paths to be relative to the new 769 * destination. 770 * 771 * @param string $source 772 * @param string $target 773 * 774 * @return ConverterInterface 775 */ 776 protected function getPathConverter($source, $target) 777 { 778 return new Converter($source, $target); 779 } 780 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body