diff --git a/.travis.yml b/.travis.yml index db4454d8..ed294142 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,3 +12,7 @@ before_script: script: - vendor/bin/phpunit tests + +matrix: + allow_failures: + - php: nightly diff --git a/src/AMP.php b/src/AMP.php index 9836e62a..b1eb5f6c 100644 --- a/src/AMP.php +++ b/src/AMP.php @@ -174,6 +174,17 @@ public function loadHtml($html, $options = []) $options['use_html5_parser'] = true; } + // By default the convertion of img into amp-anim is disabled (because of ressource cost) + // => they will be converted into amp-img instead + if (!isset($options['use_img_anim_tag'])) { + $options['use_img_anim_tag'] = false; + } + + // By default img that can't be converted are kept as it is and not removed + if (!isset($options['remove_non_converted_img_tag'])) { + $options['remove_non_converted_img_tag'] = false; + } + $this->options = $options; $this->scope = !empty($options['scope']) ? $options['scope'] : Scope::BODY_SCOPE; diff --git a/src/Pass/AmpImgFixPass.php b/src/Pass/AmpImgFixPass.php index ec9d6b55..ea4c68f4 100644 --- a/src/Pass/AmpImgFixPass.php +++ b/src/Pass/AmpImgFixPass.php @@ -75,6 +75,17 @@ function pass() $error->resolved = false; } } + elseif (in_array($error->code, [ValidationErrorCode::MANDATORY_TAG_ANCESTOR_WITH_HINT]) && + !$error->resolved && + !empty($error->dom_tag) && + strtolower($error->dom_tag->tagName) == 'img' && + !empty($this->options['remove_non_converted_img_tag']) + ) { + // Remove the offending tag + $error->dom_tag->parentNode->removeChild($error->dom_tag); + $error->addActionTaken(new ActionTakenLine('img', ActionTakenType::TAG_REMOVED)); + $error->resolved = TRUE; + } } } } diff --git a/src/Pass/IframeYouTubeTagTransformPass.php b/src/Pass/IframeYouTubeTagTransformPass.php index 67017b38..46b8bcf5 100644 --- a/src/Pass/IframeYouTubeTagTransformPass.php +++ b/src/Pass/IframeYouTubeTagTransformPass.php @@ -113,6 +113,7 @@ protected function isYouTubeIframe(DOMQuery $el) * * @param DOMQuery $el * @return string + * */ protected function getYouTubeCode(DOMQuery $el) { @@ -120,30 +121,17 @@ protected function getYouTubeCode(DOMQuery $el) $youtube_code = ''; $href = $el->attr('src'); - // @todo there seem to be a lot of ways to embed a youtube video. We probably need to capture all patterns here - // The next one is the embed code that youtube gives you - if (preg_match('&(*UTF8)/embed/([^/?\&]+)&i', $href, $matches)) { - if (!empty($matches[1])) { - $youtube_code = $matches[1]; - return $youtube_code; - } - } - - if (preg_match('&(*UTF8)youtu\.be/([^/?\&]+)&i', $href, $matches)) { - if (!empty($matches[1])) { - $youtube_code = $matches[1]; - return $youtube_code; - } - } - - if (preg_match('!(*UTF8)watch\?v=([^&]+)!i', $href, $matches)) { - if (!empty($matches[1])) { + // This regex is supposed to catch all possible way to embed youtube video + if (preg_match('%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})%i', $href, $matches)) + { + if (!empty($matches[1])) + { $youtube_code = $matches[1]; - return $youtube_code; + return htmlspecialchars($youtube_code); } } - return $youtube_code; + return htmlspecialchars($youtube_code); } /** diff --git a/src/Pass/ImgTagTransformPass.php b/src/Pass/ImgTagTransformPass.php index d8ed2488..8e28efeb 100644 --- a/src/Pass/ImgTagTransformPass.php +++ b/src/Pass/ImgTagTransformPass.php @@ -79,8 +79,9 @@ function pass() } if ($this->isPixel($el)) { $new_dom_el = $this->convertAmpPixel($el, $lineno, $context_string); - } - else { + } else if (!empty($this->options['use_amp_anim_tag']) && $this->isAnimatedImg($dom_el)) { + $new_dom_el = $this->convertAmpAnim($el, $lineno, $context_string); + } else { $new_dom_el = $this->convertAmpImg($el, $lineno, $context_string); } $this->context->addLineAssociation($new_dom_el, $lineno); @@ -139,6 +140,24 @@ protected function getLayout($el) { ? 'fixed' : 'responsive'; } + /** + * Given an animated image element returns an amp-anim element with the same attributes and children + * + * @param DOMQuery $el + * @param int $lineno + * @param string $context_string + * @return DOMElement + */ + protected function convertAmpAnim($el, $lineno, $context_string) + { + $dom_el = $el->get(0); + $new_dom_el = $this->cloneAndRenameDomElement($dom_el, 'amp-anim'); + $new_el = $el->prev(); + $this->setLayoutIfNoLayout($new_el, 'responsive'); + $this->addActionTaken(new ActionTakenLine('img', ActionTakenType::IMG_ANIM_CONVERTED, $lineno, $context_string)); + return $new_dom_el; + } + /** * Given an image src attribute, try to get its dimensions * Returns false on failure @@ -188,6 +207,87 @@ protected function isPixel(DOMQuery $el) return $el->attr('width') === '1' && $el->attr('height') === '1'; } + /** + * Detects if the img is animated. In that case we convert to instead of + * @param \DOMElement $el + * @return bool + */ + protected function isAnimatedImg(\DOMElement $el) + { + $animated_type = ['gif', 'png']; + if (!$el->hasAttribute('src')) { + return true; + } + + $src = trim($el->getAttribute('src')); + if (preg_match('/\.([a-z0-9]+)$/i', parse_url($src,PHP_URL_PATH), $match)) { + if (!empty($match[1]) && in_array(strtolower($match[1]), $animated_type)) { + if ($match[1] === "gif") { + if ($this->isAnimatedGif($src)) { + return true; + } else { + return false; + } + } + if ($this->isApng($src)) { + return true; + } + } + } + + return false; + } + + /** + * Identifies APNGs + * Written by Coda, functionified by Foone/Popcorn Mariachi#!9i78bPeIxI + * This code is in the public domain + * + * @see http://stackoverflow.com/a/4525194 + * @see http://foone.org/apng/identify_apng.php + * + * @param string $src The filename + * @return bool true if the file is an APMG + */ + function isApng($src) + { + $img_bytes = @file_get_contents($src); + if ($img_bytes) { + if (strpos(substr($img_bytes, 0, strpos($img_bytes, 'IDAT')), 'acTL') !== false) { + return true; + } + } + return false; + } + + /** + * Detects if the gif image is animated or not + * source: http://php.net/manual/en/function.imagecreatefromgif.php#104473 + * + * @param string $filename + * @return bool + */ + function isAnimatedGif($filename) { + if (!($fh = @fopen($filename, 'rb'))) + return FALSE; + $count = 0; + //an animated gif contains multiple "frames", with each frame having a + //header made up of: + // * a static 4-byte sequence (\x00\x21\xF9\x04) + // * 4 variable bytes + // * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?) + + // We read through the file til we reach the end of the file, or we've found + // at least 2 frame headers + while (!feof($fh) && $count < 2) { + $chunk = fread($fh, 1024 * 100); //read 100kb at a time + $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches); + } + + fclose($fh); + return $count > 1; + } + /** * @param string $src * @return boolean|string diff --git a/src/Utility/ActionTakenType.php b/src/Utility/ActionTakenType.php index 0a9a8817..52462b5c 100644 --- a/src/Utility/ActionTakenType.php +++ b/src/Utility/ActionTakenType.php @@ -25,6 +25,7 @@ class ActionTakenType const PROPERTY_REMOVED_ATTRIBUTE_REMOVED = 'property value pair was removed from attribute due to validation issues. The resulting attribute was empty and was also removed.'; const IMG_CONVERTED = 'tag was converted to the amp-img tag.'; const IMG_PIXEL_CONVERTED = 'tag was converted to the amp-pixel tag.'; + const IMG_ANIM_CONVERTED = 'tag was converted to the amp-anim tag.'; const IMG_COULD_NOT_BE_CONVERTED = 'tag could NOT be converted to the amp-img tag as the image is not accessible.'; const INSTAGRAM_CONVERTED = 'instagram embed code was converted to the amp-instagram tag.'; const PINTEREST_CONVERTED = 'pinterest embed code was converted to the amp-pinterest tag.'; diff --git a/tests/test-data/fragment-html/img-anim-test-fragment.html b/tests/test-data/fragment-html/img-anim-test-fragment.html new file mode 100644 index 00000000..26cbce10 --- /dev/null +++ b/tests/test-data/fragment-html/img-anim-test-fragment.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/tests/test-data/fragment-html/img-anim-test-fragment.html.options.json b/tests/test-data/fragment-html/img-anim-test-fragment.html.options.json new file mode 100644 index 00000000..e4ef92a8 --- /dev/null +++ b/tests/test-data/fragment-html/img-anim-test-fragment.html.options.json @@ -0,0 +1,4 @@ +{ + "use_amp_anim_tag" : true, + "remove_non_converted_img_tag" : true +} diff --git a/tests/test-data/fragment-html/img-anim-test-fragment.html.out b/tests/test-data/fragment-html/img-anim-test-fragment.html.out new file mode 100644 index 00000000..772f89d5 --- /dev/null +++ b/tests/test-data/fragment-html/img-anim-test-fragment.html.out @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + +ORIGINAL HTML +--------------- +Line 1: +Line 2: +Line 3: +Line 4: +Line 5: +Line 6: +Line 7: +Line 8: +Line 9: +Line 10: +Line 11: +Line 12: +Line 13: +Line 14: +Line 15: + + +Transformations made from HTML tags to AMP custom tags +------------------------------------------------------- + + at line 2 + ACTION TAKEN: img tag was converted to the amp-anim tag. + + at line 5 + ACTION TAKEN: img tag was converted to the amp-img tag. + + at line 8 + ACTION TAKEN: img tag was converted to the amp-anim tag. + + at line 11 + ACTION TAKEN: img tag was converted to the amp-img tag. + + at line 14 + ACTION TAKEN: img tag could NOT be converted to the amp-img tag as the image is not accessible. + + +AMP-HTML Validation Issues and Fixes +------------------------------------- +FAIL + + on line 14 +- The tag 'img' may only appear as a descendant of tag 'noscript'. Did you mean 'amp-img'? + [code: MANDATORY_TAG_ANCESTOR_WITH_HINT category: DISALLOWED_HTML_WITH_AMP_EQUIVALENT see: https://www.ampproject.org/docs/reference/amp-img.html] + ACTION TAKEN: img tag was removed due to validation issues. + +COMPONENT NAMES WITH JS PATH +------------------------------ +'amp-anim', include path 'https://cdn.ampproject.org/v0/amp-anim-0.1.js' + diff --git a/tests/test-data/fragment-html/img-test-fragment.html b/tests/test-data/fragment-html/img-test-fragment.html index 348842c8..69554435 100644 --- a/tests/test-data/fragment-html/img-test-fragment.html +++ b/tests/test-data/fragment-html/img-test-fragment.html @@ -6,7 +6,7 @@ - + @@ -22,4 +22,7 @@ - \ No newline at end of file + + + + diff --git a/tests/test-data/fragment-html/img-test-fragment.html.out b/tests/test-data/fragment-html/img-test-fragment.html.out index 62092be5..20a30bb1 100644 --- a/tests/test-data/fragment-html/img-test-fragment.html.out +++ b/tests/test-data/fragment-html/img-test-fragment.html.out @@ -6,7 +6,7 @@ - + @@ -24,6 +24,10 @@ + + + + ORIGINAL HTML --------------- Line 1: @@ -34,7 +38,7 @@ Line 5: Line 6: Line 7: Line 8: -Line 9: +Line 9: Line 10: Line 11: Line 12: @@ -51,6 +55,10 @@ Line 22: +Line 26: +Line 27: +Line 28: +Line 29: Transformations made from HTML tags to AMP custom tags @@ -68,6 +76,9 @@ Transformations made from HTML tags to AMP custom tags at line 25 ACTION TAKEN: img tag was converted to the amp-pixel tag. + at line 28 + ACTION TAKEN: img tag was converted to the amp-img tag. + AMP-HTML Validation Issues and Fixes ------------------------------------- diff --git a/tests/test-data/fragment-html/youtube-bad-fragment.html b/tests/test-data/fragment-html/youtube-bad-fragment.html new file mode 100644 index 00000000..ff4f5921 --- /dev/null +++ b/tests/test-data/fragment-html/youtube-bad-fragment.html @@ -0,0 +1,4 @@ + diff --git a/tests/test-data/fragment-html/youtube-bad-fragment.html.out b/tests/test-data/fragment-html/youtube-bad-fragment.html.out new file mode 100644 index 00000000..259906ea --- /dev/null +++ b/tests/test-data/fragment-html/youtube-bad-fragment.html.out @@ -0,0 +1,27 @@ + + + +ORIGINAL HTML +--------------- +Line 1: +Line 5: + + +Transformations made from HTML tags to AMP custom tags +------------------------------------------------------- + + \ No newline at end of file + + + + + + + + + + + + + diff --git a/tests/test-data/fragment-html/youtube-fragment.html.out b/tests/test-data/fragment-html/youtube-fragment.html.out index d711b65a..21365f43 100644 --- a/tests/test-data/fragment-html/youtube-fragment.html.out +++ b/tests/test-data/fragment-html/youtube-fragment.html.out @@ -1,11 +1,55 @@ + + + + + + + + + + + + + ORIGINAL HTML --------------- -Line 1: +Line 1: +Line 5: +Line 6: +Line 10: +Line 11: +Line 15: +Line 16: +Line 20: +Line 21: +Line 25: +Line 26: +Line 30: +Line 31: +Line 35: Transformations made from HTML tags to AMP custom tags @@ -14,6 +58,24 @@ Transformations made from HTML tags to AMP custom tags