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/README.md b/README.md index 405390dc..3ffbb9dd 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ $ ./amp-console amp:convert sample-html/several_errors.html --full-document ``` Note that you need to provide `--full-document` if you're providing a full html document file for conversion. -Lets see the output output of the first example command above. The first few lines is the AMPized HTML provided by our library. The rest of the headings are self explanatory. +Lets see the output of the first example command above. The first few lines is the AMPized HTML provided by our library. The rest of the headings are self explanatory. ```html $ cd diff --git a/src/AMP.php b/src/AMP.php index 9836e62a..e852ade7 100644 --- a/src/AMP.php +++ b/src/AMP.php @@ -47,6 +47,7 @@ class AMP // The StandardScanPass should be first after all transform passes // The StandardFixPass should be after StandardScanPass + // The ObjectVideoTagTransformPass should be after all Object transform passes public $passes = [ 'Lullabot\AMP\Pass\PreliminaryPass', // Removes user blacklisted tags 'Lullabot\AMP\Pass\ImgTagTransformPass', @@ -63,6 +64,9 @@ class AMP 'Lullabot\AMP\Pass\PinterestTagTransformPass', 'Lullabot\AMP\Pass\FacebookNonIframeTransformPass', 'Lullabot\AMP\Pass\TwitterTransformPass', + 'Lullabot\AMP\Pass\ObjectYouTubeTagTransformPass', + 'Lullabot\AMP\Pass\ObjectVimeoTagTransformPass', + 'Lullabot\AMP\Pass\ObjectVideoTagTransformPass', 'Lullabot\AMP\Pass\StandardScanPass', 'Lullabot\AMP\Pass\StandardFixPass', 'Lullabot\AMP\Pass\AmpImgFixPass', @@ -174,8 +178,24 @@ 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; + } + + // By default the scope is 'body' + if (!isset($options['scope'])) { + $options['scope'] = Scope::BODY_SCOPE; + } + $this->options = $options; - $this->scope = !empty($options['scope']) ? $options['scope'] : Scope::BODY_SCOPE; + $this->scope = $options['scope']; // Currently we only support these two scopes if (!in_array($this->scope, [Scope::HTML_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/BaseFacebookPass.php b/src/Pass/BaseFacebookPass.php new file mode 100644 index 00000000..0947d95f --- /dev/null +++ b/src/Pass/BaseFacebookPass.php @@ -0,0 +1,49 @@ +isValidVideoUrl($src)) { // A facebook video can be embedded as a post. Doing that enables the video "card" to display if ($el->attr('data-show-text') !== "false") { $embed_as = 'post'; @@ -118,8 +117,8 @@ protected function getFacebookEmbedAttrs(DOMQuery $el) } else { $embed_as = 'video'; } - } // e.g. https://www.facebook.com/20531316728/posts/10154009990506729/ - else if (preg_match('&(*UTF8)facebook\.com/.*/posts/\d+/?&i', $src)) { + } + elseif ($this->isValidPostUrl($src)) { $embed_as = 'post'; } else { return false; diff --git a/src/Pass/IframeFacebookTagTransformPass.php b/src/Pass/IframeFacebookTagTransformPass.php index 281aa377..f3883247 100644 --- a/src/Pass/IframeFacebookTagTransformPass.php +++ b/src/Pass/IframeFacebookTagTransformPass.php @@ -54,7 +54,7 @@ * @see https://github.com/ampproject/amphtml/blob/master/extensions/amp-facebook/amp-facebook.md * @see https://github.com/ampproject/amphtml/blob/master/extensions/amp-facebook/0.1/validator-amp-facebook.protoascii */ -class IframeFacebookTagTransformPass extends BasePass +class IframeFacebookTagTransformPass extends BaseFacebookPass { const DEFAULT_ASPECT_RATIO = 1.7778; const DEFAULT_WIDTH = 500; @@ -108,16 +108,15 @@ protected function setStandardFacebookParameters(DOMQuery $el) return false; } - // e.g https://www.facebook.com/facebook/videos/10153231379946729/ - if (preg_match('&(*UTF8)facebook\.com/facebook/videos/\d+/?&i', $query_arr['href'])) { + if ($this->isValidVideoUrl($query_arr['href'])) { // A facebook video can be embedded as a post. Doing that enables the video "card" to display if (isset($query_arr['show_text']) && $query_arr['show_text'] !== "false") { $embed_as = 'post'; } else { $embed_as = 'video'; } - } // e.g. https://www.facebook.com/20531316728/posts/10154009990506729/ - else if (preg_match('&(*UTF8)facebook\.com/.*/posts/\d+/?&i', $query_arr['href'])) { + } + elseif ($this->isValidPostUrl($query_arr['href'])) { $embed_as = 'post'; } else { return false; diff --git a/src/Pass/IframeYouTubeTagTransformPass.php b/src/Pass/IframeYouTubeTagTransformPass.php index a607d93e..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 89757773..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); @@ -121,11 +122,42 @@ protected function convertAmpImg($el, $lineno, $context_string) $dom_el = $el->get(0); $new_dom_el = $this->cloneAndRenameDomElement($dom_el, 'amp-img'); $new_el = $el->prev(); - $this->setLayoutIfNoLayout($new_el, 'responsive'); + $this->setLayoutIfNoLayout($new_el, $this->getLayout($el)); $this->addActionTaken(new ActionTakenLine('img', ActionTakenType::IMG_CONVERTED, $lineno, $context_string)); return $new_dom_el; } + /** + * Given an image DOMQuery + * Returns whether the image should have 'fixed' or 'responsive' layout + * + * @param DOMQuery $el + * @return string + */ + protected function getLayout($el) { + return (isset($this->options['img_max_fixed_layout_width']) + && $this->options['img_max_fixed_layout_width'] >= $el->attr('width')) + ? '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 @@ -175,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 @@ -217,7 +330,7 @@ protected function setResponsiveImgHeightAndWidth(DOMQuery $el) $wcss = new CssLengthAndUnit($el->attr('width'), false); $hcss = new CssLengthAndUnit($el->attr('height'), false); - if ($wcss->is_set && $wcss->is_valid && $wcss->is_set && $wcss->is_valid && $wcss->unit == $hcss->unit) { + if ($wcss->is_set && $wcss->is_valid && $hcss->is_set && $hcss->is_valid && $wcss->unit == $hcss->unit) { return true; } diff --git a/src/Pass/ObjectVideoTagTransformPass.php b/src/Pass/ObjectVideoTagTransformPass.php new file mode 100644 index 00000000..fde2f6eb --- /dev/null +++ b/src/Pass/ObjectVideoTagTransformPass.php @@ -0,0 +1,112 @@ + tags + * + * This is what a video embed looks like: + * + * + * + * + * + * + */ +class ObjectVideoTagTransformPass extends BasePass +{ + function pass() + { + $all_objects = $this->q->find('object:not(noscript object)'); + /** @var DOMQuery $el */ + foreach ($all_objects as $el) { + /** @var \DOMElement $dom_el */ + $dom_el = $el->get(0); + $lineno = $this->getLineNo($dom_el); + $context_string = $this->getContextString($dom_el); + + $actionTakenType = ''; + + if ($this->isVideoObject($el)) { + $video_url = $this->getVideoUrl($el); + + if (empty($video_url)) { + continue; + } + + $video_url = htmlspecialchars($video_url, ENT_QUOTES); + $el->after("{$video_url}"); + $new_dom_el = $el->next()->get(0); + + $actionTakenType = ActionTakenType::OBJECT_CONVERTED_TO_A; + } else { + continue; + } + + // Remove the object and its children + $el->removeChildren()->remove(); + $this->addActionTaken(new ActionTakenLine('object', $actionTakenType, $lineno, $context_string)); + $this->context->addLineAssociation($new_dom_el, $lineno); + + } + + return $this->transformations; + } + + protected function isVideoObject(DOMQuery $el) + { + $params = $el->find('param'); + foreach ($params as $param) { + if ($param->attr('name') == 'movie') { + return true; + } + } + return false; + } + + /** + * + * Get the Video Url + * + * @param DOMQuery $el + * @return string + */ + protected function getVideoUrl(DOMQuery $el) + { + $matches = []; + $video_url = ''; + $params = $el->find('param'); + + foreach ($params as $param) { + if ($param->attr('name') == 'movie') { + return $param->attr('value'); + } + } + + return $video_url; + } +} diff --git a/src/Pass/ObjectVimeoTagTransformPass.php b/src/Pass/ObjectVimeoTagTransformPass.php new file mode 100644 index 00000000..260ac463 --- /dev/null +++ b/src/Pass/ObjectVimeoTagTransformPass.php @@ -0,0 +1,127 @@ + tags which don't have noscript as an ancestor to tags + * + * This is what a vimeo embed looks like: + * + * + * + * + * + * + * + * @see https://github.com/ampproject/amphtml/blob/master/extensions/amp-vimeo/amp-vimeo.md + * + */ +class ObjectVimeoTagTransformPass extends BasePass +{ + function pass() + { + $all_objects = $this->q->find('object:not(noscript object)'); + /** @var DOMQuery $el */ + foreach ($all_objects as $el) { + /** @var \DOMElement $dom_el */ + $dom_el = $el->get(0); + $lineno = $this->getLineNo($dom_el); + $context_string = $this->getContextString($dom_el); + + $actionTakenType = ''; + + if ($this->isVimeoObject($el)) { + $vimeo_code = $this->getVimeoCode($el); + + // If we couldnt find a vimeo videoid then we abort + if (empty($vimeo_code)) { + continue; + } + + $el->after(''); + $new_dom_el = $el->next()->get(0); + + $actionTakenType = ActionTakenType::VIMEO_OBJECT_CONVERTED; + + } else { + continue; + } + + // Remove the object and its children + $el->removeChildren()->remove(); + $this->addActionTaken(new ActionTakenLine('object', $actionTakenType, $lineno, $context_string)); + $this->context->addLineAssociation($new_dom_el, $lineno); + + } + + return $this->transformations; + } + + protected function isVimeoObject(DOMQuery $el) + { + $params = $el->find('param'); + foreach ($params as $param) { + if ($param->attr('name') == 'movie') { + $param_value = $param->attr('value'); + if (preg_match('&(*UTF8)(vimeo\.com)&i', $param_value)) { + return true; + } + } + } + return false; + } + + /** + * + * Get the Vimeo Code + * + * @param DOMQuery $el + * @return string + */ + protected function getVimeoCode(DOMQuery $el) + { + $matches = []; + $vimeo_code = ''; + $params = $el->find('param'); + + foreach ($params as $param) { + if ($param->attr('name') == 'movie') { + $param_value = $param->attr('value'); + + $pattern = '#http://(?:\w+.)?vimeo.com/(?:video/|moogaloop\.swf\?clip_id=)(\w+)#i'; + if (preg_match($pattern, $param_value, $matches)) { + if (!empty($matches[1])) { + $vimeo_code = $matches[1]; + return $vimeo_code; + } + } + } + } + + return $vimeo_code; + } +} diff --git a/src/Pass/ObjectYouTubeTagTransformPass.php b/src/Pass/ObjectYouTubeTagTransformPass.php new file mode 100644 index 00000000..b1de2e3f --- /dev/null +++ b/src/Pass/ObjectYouTubeTagTransformPass.php @@ -0,0 +1,163 @@ + tags which don't have noscript as an ancestor to tags + * + * This is what a youtube embed looks like: + * + * + * + * + * + * + * + * @see https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube/amp-youtube.md + * + */ +class ObjectYouTubeTagTransformPass extends BasePass +{ + /** + * A standard youtube video aspect ratio + * @var float + */ + const DEFAULT_ASPECT_RATIO = 1.7778; + const DEFAULT_VIDEO_WIDTH = 560; + const DEFAULT_VIDEO_HEIGHT = 315; + + function pass() + { + $all_objects = $this->q->find('object:not(noscript object)'); + /** @var DOMQuery $el */ + foreach ($all_objects as $el) { + /** @var \DOMElement $dom_el */ + $dom_el = $el->get(0); + $lineno = $this->getLineNo($dom_el); + $context_string = $this->getContextString($dom_el); + + $actionTakenType = ''; + + if ($this->isYouTubeObject($el)) { + $youtube_code = $this->getYouTubeCode($el); + + // If we couldnt find a youtube videoid then we abort + if (empty($youtube_code)) { + continue; + } + + $el->after(""); + + $new_el = $el->next(); + $new_dom_el = $new_el->get(0); + + $actionTakenType = ActionTakenType::YOUTUBE_OBJECT_CONVERTED; + + $this->setStandardAttributesFrom($el, $new_el, self::DEFAULT_VIDEO_WIDTH, self::DEFAULT_VIDEO_HEIGHT, self::DEFAULT_ASPECT_RATIO); + + } else { + continue; + } + + // Remove the object and its children + $el->removeChildren()->remove(); + $this->addActionTaken(new ActionTakenLine('object', $actionTakenType, $lineno, $context_string)); + $this->context->addLineAssociation($new_dom_el, $lineno); + + } + + return $this->transformations; + } + + protected function isYouTubeObject(DOMQuery $el) + { + $params = $el->find('param'); + foreach ($params as $param) { + if ($param->attr('name') == 'movie') { + $param_value = $param->attr('value'); + if (preg_match('&(*UTF8)(youtube\.com|youtu\.be)&i', $param_value)) { + return true; + } + } + } + + return false; + } + + /** + * + * Get the youtube videoid + * + * @param DOMQuery $el + * @return string + */ + protected function getYouTubeCode(DOMQuery $el) + { + $matches = []; + $youtube_code = ''; + $params = $el->find('param'); + + foreach ($params as $param) { + if ($param->attr('name') == 'movie') { + $param_value = $param->attr('value'); + + $pattern = + '~(?#!js YouTubeId Rev:20160125_1800) + # Match non-linked youtube URL in the wild. (Rev:20130823) + https?:// # Required scheme. Either http or https. + (?:[0-9A-Z-]+\.)? # Optional subdomain. + (?: # Group host alternatives. + youtu\.be/ # Either youtu.be, + | youtube # or youtube.com or + (?:-nocookie)? # youtube-nocookie.com + \.com # followed by + \S*? # Allow anything up to VIDEO_ID, + [^\w\s-] # but char before ID is non-ID char. + ) # End host alternatives. + ([\w-]{11}) # $1: VIDEO_ID is exactly 11 chars. + (?=[^\w-]|$) # Assert next char is non-ID or EOS. + (?! # Assert URL is not pre-linked. + [?=&+%\w.-]* # Allow URL (query) remainder. + (?: # Group pre-linked alternatives. + [\'"][^<>]*> # Either inside a start tag, + | # or inside element text contents. + ) # End recognized pre-linked alts. + ) # End negative lookahead assertion. + [?=&+%\w.-]* # Consume any URL (query) remainder. + ~ix'; + if (preg_match($pattern, $param_value, $matches)) { + if (!empty($matches[1])) { + $youtube_code = $matches[1]; + return $youtube_code; + } + } + } + } + + return $youtube_code; + } +} diff --git a/src/Pass/StandardFixPass.php b/src/Pass/StandardFixPass.php index c5771d8d..fc52c4a4 100644 --- a/src/Pass/StandardFixPass.php +++ b/src/Pass/StandardFixPass.php @@ -58,6 +58,11 @@ class StandardFixPass extends BasePass public function pass() { + $all_ampad = $this->q->top()->find('amp-ad'); + if ($all_ampad->length > 0) { + $this->addComponentJsToHead('amp-ad'); + } + /** @var SValidationError $error */ foreach ($this->validation_result->errors as $error) { // If the error was resolved, continue diff --git a/src/Spec/validator-generated.php b/src/Spec/validator-generated.php index 082bb58a..6523ef92 100644 --- a/src/Spec/validator-generated.php +++ b/src/Spec/validator-generated.php @@ -2358,33 +2358,39 @@ public static function createValidationRules() { $o_564->tag_name = 'o:p'; $o_0->tags[] = $o_564; $o_565 = new TagSpec(); - $o_565->tag_name = 'amp-ad'; + $o_565->tag_name = 'script'; + $o_565->spec_name = 'amp-ad extension .js script'; + $o_565->mandatory_parent = 'head'; $o_566 = new AttrSpec(); - $o_566->name = 'alt'; + $o_566->name = 'async'; + $o_566->mandatory = true; + $o_566->value = ''; $o_565->attrs[] = $o_566; $o_567 = new AttrSpec(); - $o_567->name = 'json'; + $o_567->name = 'custom-element'; + $o_567->mandatory = true; + $o_567->value = 'amp-ad'; + $o_567->dispatch_key = true; $o_565->attrs[] = $o_567; $o_568 = new AttrSpec(); $o_568->name = 'src'; - $o_569 = new UrlSpec(); - $o_569->allowed_protocol = ['https']; - $o_569->allow_relative = true; - $o_568->value_url = $o_569; + $o_568->mandatory = true; + $o_568->value_regex = 'https://cdn\\.ampproject\\.org/v0/amp-ad-(latest|0\\.1).js'; $o_565->attrs[] = $o_568; - $o_570 = new AttrSpec(); - $o_570->name = 'type'; - $o_570->mandatory = true; - $o_565->attrs[] = $o_570; - $o_565->attr_lists = ['extended-amp-global']; - $o_565->spec_url = 'https://www.ampproject.org/docs/reference/amp-ad.html'; - $o_571 = new AmpLayout(); - $o_571->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; - $o_565->amp_layout = $o_571; - $o_565->disallowed_ancestor = ['amp-sidebar']; + $o_569 = new AttrSpec(); + $o_569->name = 'type'; + $o_569->value = 'text/javascript'; + $o_565->attrs[] = $o_569; + $o_565->spec_url = 'https://www.ampproject.org/docs/reference/extended/amp-ad.html'; + $o_570 = new CdataSpec(); + $o_571 = new BlackListedCDataRegex(); + $o_571->regex = '.'; + $o_571->error_message = 'contents'; + $o_570->blacklisted_cdata_regex[] = $o_571; + $o_565->cdata = $o_570; $o_0->tags[] = $o_565; $o_572 = new TagSpec(); - $o_572->tag_name = 'amp-embed'; + $o_572->tag_name = 'amp-ad'; $o_573 = new AttrSpec(); $o_573->name = 'alt'; $o_572->attrs[] = $o_573; @@ -2403,723 +2409,717 @@ public static function createValidationRules() { $o_577->mandatory = true; $o_572->attrs[] = $o_577; $o_572->attr_lists = ['extended-amp-global']; - $o_572->spec_url = 'https://www.ampproject.org/docs/reference/amp-embed.html'; + $o_572->spec_url = 'https://www.ampproject.org/docs/reference/amp-ad.html'; $o_578 = new AmpLayout(); $o_578->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; $o_572->amp_layout = $o_578; $o_572->disallowed_ancestor = ['amp-sidebar']; $o_0->tags[] = $o_572; $o_579 = new TagSpec(); - $o_579->tag_name = 'amp-img'; + $o_579->tag_name = 'amp-embed'; $o_580 = new AttrSpec(); $o_580->name = 'alt'; $o_579->attrs[] = $o_580; $o_581 = new AttrSpec(); - $o_581->name = 'attribution'; + $o_581->name = 'json'; $o_579->attrs[] = $o_581; $o_582 = new AttrSpec(); - $o_582->name = 'placeholder'; + $o_582->name = 'src'; + $o_583 = new UrlSpec(); + $o_583->allowed_protocol = ['https']; + $o_583->allow_relative = true; + $o_582->value_url = $o_583; $o_579->attrs[] = $o_582; - $o_579->attr_lists = ['extended-amp-global', 'mandatory-src-or-srcset']; - $o_579->spec_url = 'https://www.ampproject.org/docs/reference/amp-img.html'; - $o_583 = new AmpLayout(); - $o_583->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; - $o_579->amp_layout = $o_583; + $o_584 = new AttrSpec(); + $o_584->name = 'type'; + $o_584->mandatory = true; + $o_579->attrs[] = $o_584; + $o_579->attr_lists = ['extended-amp-global']; + $o_579->spec_url = 'https://www.ampproject.org/docs/reference/amp-embed.html'; + $o_585 = new AmpLayout(); + $o_585->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; + $o_579->amp_layout = $o_585; + $o_579->disallowed_ancestor = ['amp-sidebar']; $o_0->tags[] = $o_579; - $o_584 = new TagSpec(); - $o_584->tag_name = 'amp-pixel'; - $o_585 = new AttrSpec(); - $o_585->name = 'src'; - $o_585->mandatory = true; - $o_586 = new UrlSpec(); - $o_586->allowed_protocol = ['https']; - $o_586->allow_relative = true; - $o_585->value_url = $o_586; - $o_584->attrs[] = $o_585; - $o_584->attr_lists = ['extended-amp-global']; - $o_584->spec_url = 'https://www.ampproject.org/docs/reference/amp-pixel.html'; - $o_587 = new AmpLayout(); - $o_587->supported_layouts = [AmpLayoutLayout::FIXED, AmpLayoutLayout::NODISPLAY]; - $o_587->defines_default_width = true; - $o_587->defines_default_height = true; - $o_584->amp_layout = $o_587; - $o_584->disallowed_ancestor = ['amp-sidebar']; - $o_0->tags[] = $o_584; - $o_588 = new TagSpec(); - $o_588->tag_name = 'amp-video'; + $o_586 = new TagSpec(); + $o_586->tag_name = 'amp-img'; + $o_587 = new AttrSpec(); + $o_587->name = 'alt'; + $o_586->attrs[] = $o_587; + $o_588 = new AttrSpec(); + $o_588->name = 'attribution'; + $o_586->attrs[] = $o_588; $o_589 = new AttrSpec(); - $o_589->name = 'alt'; - $o_588->attrs[] = $o_589; - $o_590 = new AttrSpec(); - $o_590->name = 'attribution'; - $o_588->attrs[] = $o_590; - $o_591 = new AttrSpec(); - $o_591->name = 'autoplay'; - $o_591->value = ''; - $o_588->attrs[] = $o_591; + $o_589->name = 'placeholder'; + $o_586->attrs[] = $o_589; + $o_586->attr_lists = ['extended-amp-global', 'mandatory-src-or-srcset']; + $o_586->spec_url = 'https://www.ampproject.org/docs/reference/amp-img.html'; + $o_590 = new AmpLayout(); + $o_590->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; + $o_586->amp_layout = $o_590; + $o_0->tags[] = $o_586; + $o_591 = new TagSpec(); + $o_591->tag_name = 'amp-pixel'; $o_592 = new AttrSpec(); - $o_592->name = 'controls'; - $o_592->value = ''; - $o_588->attrs[] = $o_592; - $o_593 = new AttrSpec(); - $o_593->name = 'loop'; - $o_593->value = ''; - $o_588->attrs[] = $o_593; - $o_594 = new AttrSpec(); - $o_594->name = 'muted'; - $o_594->value = ''; - $o_588->attrs[] = $o_594; - $o_595 = new AttrSpec(); - $o_595->name = 'placeholder'; - $o_588->attrs[] = $o_595; + $o_592->name = 'src'; + $o_592->mandatory = true; + $o_593 = new UrlSpec(); + $o_593->allowed_protocol = ['https']; + $o_593->allow_relative = true; + $o_592->value_url = $o_593; + $o_591->attrs[] = $o_592; + $o_591->attr_lists = ['extended-amp-global']; + $o_591->spec_url = 'https://www.ampproject.org/docs/reference/amp-pixel.html'; + $o_594 = new AmpLayout(); + $o_594->supported_layouts = [AmpLayoutLayout::FIXED, AmpLayoutLayout::NODISPLAY]; + $o_594->defines_default_width = true; + $o_594->defines_default_height = true; + $o_591->amp_layout = $o_594; + $o_591->disallowed_ancestor = ['amp-sidebar']; + $o_0->tags[] = $o_591; + $o_595 = new TagSpec(); + $o_595->tag_name = 'amp-video'; $o_596 = new AttrSpec(); - $o_596->name = 'poster'; - $o_588->attrs[] = $o_596; + $o_596->name = 'alt'; + $o_595->attrs[] = $o_596; $o_597 = new AttrSpec(); - $o_597->name = 'preload'; - $o_597->value_regex = '(none|metadata|auto|)'; - $o_588->attrs[] = $o_597; + $o_597->name = 'attribution'; + $o_595->attrs[] = $o_597; $o_598 = new AttrSpec(); - $o_598->name = 'src'; - $o_599 = new UrlSpec(); - $o_599->allowed_protocol = ['https']; - $o_599->allow_relative = true; - $o_598->value_url = $o_599; - $o_588->attrs[] = $o_598; - $o_588->attr_lists = ['extended-amp-global']; - $o_588->spec_url = 'https://www.ampproject.org/docs/reference/amp-video.html'; - $o_600 = new AmpLayout(); - $o_600->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; - $o_588->amp_layout = $o_600; - $o_588->disallowed_ancestor = ['amp-sidebar']; - $o_0->tags[] = $o_588; - $o_601 = new TagSpec(); - $o_601->tag_name = 'script'; - $o_601->spec_name = 'amp-access extension .js script'; - $o_601->mandatory_parent = 'head'; + $o_598->name = 'autoplay'; + $o_598->value = ''; + $o_595->attrs[] = $o_598; + $o_599 = new AttrSpec(); + $o_599->name = 'controls'; + $o_599->value = ''; + $o_595->attrs[] = $o_599; + $o_600 = new AttrSpec(); + $o_600->name = 'loop'; + $o_600->value = ''; + $o_595->attrs[] = $o_600; + $o_601 = new AttrSpec(); + $o_601->name = 'muted'; + $o_601->value = ''; + $o_595->attrs[] = $o_601; $o_602 = new AttrSpec(); - $o_602->name = 'async'; - $o_602->mandatory = true; - $o_602->value = ''; - $o_601->attrs[] = $o_602; + $o_602->name = 'placeholder'; + $o_595->attrs[] = $o_602; $o_603 = new AttrSpec(); - $o_603->name = 'custom-element'; - $o_603->mandatory = true; - $o_603->value = 'amp-access'; - $o_603->dispatch_key = true; - $o_601->attrs[] = $o_603; + $o_603->name = 'poster'; + $o_595->attrs[] = $o_603; $o_604 = new AttrSpec(); - $o_604->name = 'src'; - $o_604->mandatory = true; - $o_604->value_regex = 'https://cdn\\.ampproject\\.org/v0/amp-access-(latest|0\\.1).js'; - $o_601->attrs[] = $o_604; + $o_604->name = 'preload'; + $o_604->value_regex = '(none|metadata|auto|)'; + $o_595->attrs[] = $o_604; $o_605 = new AttrSpec(); - $o_605->name = 'type'; - $o_605->value = 'text/javascript'; - $o_601->attrs[] = $o_605; - $o_601->spec_url = 'https://www.ampproject.org/docs/reference/extended/amp-access.html'; - $o_606 = new CdataSpec(); - $o_607 = new BlackListedCDataRegex(); - $o_607->regex = '.'; - $o_607->error_message = 'contents'; - $o_606->blacklisted_cdata_regex[] = $o_607; - $o_601->cdata = $o_606; - $o_0->tags[] = $o_601; + $o_605->name = 'src'; + $o_606 = new UrlSpec(); + $o_606->allowed_protocol = ['https']; + $o_606->allow_relative = true; + $o_605->value_url = $o_606; + $o_595->attrs[] = $o_605; + $o_595->attr_lists = ['extended-amp-global']; + $o_595->spec_url = 'https://www.ampproject.org/docs/reference/amp-video.html'; + $o_607 = new AmpLayout(); + $o_607->supported_layouts = [AmpLayoutLayout::FILL, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::FLEX_ITEM, AmpLayoutLayout::NODISPLAY, AmpLayoutLayout::RESPONSIVE]; + $o_595->amp_layout = $o_607; + $o_595->disallowed_ancestor = ['amp-sidebar']; + $o_0->tags[] = $o_595; $o_608 = new TagSpec(); $o_608->tag_name = 'script'; - $o_608->spec_name = 'amp-access extension .json script'; + $o_608->spec_name = 'amp-access extension .js script'; $o_608->mandatory_parent = 'head'; $o_609 = new AttrSpec(); - $o_609->name = 'id'; + $o_609->name = 'async'; $o_609->mandatory = true; - $o_609->value = 'amp-access'; - $o_609->dispatch_key = true; + $o_609->value = ''; $o_608->attrs[] = $o_609; $o_610 = new AttrSpec(); - $o_610->name = 'type'; + $o_610->name = 'custom-element'; $o_610->mandatory = true; - $o_610->value = 'application/json'; + $o_610->value = 'amp-access'; + $o_610->dispatch_key = true; $o_608->attrs[] = $o_610; - $o_608->spec_url = 'https://www.ampproject.org/docs/reference/extended/amp-access-spec.html'; - $o_611 = new CdataSpec(); - $o_612 = new BlackListedCDataRegex(); - $o_612->regex = ' + diff --git a/tests/test-data/fragment-html/facebook-iframe-fragment.html.out b/tests/test-data/fragment-html/facebook-iframe-fragment.html.out index 0806b80e..c896abd2 100644 --- a/tests/test-data/fragment-html/facebook-iframe-fragment.html.out +++ b/tests/test-data/fragment-html/facebook-iframe-fragment.html.out @@ -7,6 +7,8 @@ + + ORIGINAL HTML @@ -26,7 +28,11 @@ Line 12: Line 15: -Line 16: +Line 16: +Line 17: +Line 20: Transformations made from HTML tags to AMP custom tags @@ -41,6 +47,9 @@ Transformations made from HTML tags to AMP custom tags \ No newline at end of file diff --git a/tests/test-data/fragment-html/youtube-02-fragment.html.out b/tests/test-data/fragment-html/youtube-02-fragment.html.out new file mode 100644 index 00000000..3a7c33ac --- /dev/null +++ b/tests/test-data/fragment-html/youtube-02-fragment.html.out @@ -0,0 +1,25 @@ + + +ORIGINAL HTML +--------------- +Line 1: + + +Transformations made from HTML tags to AMP custom tags +------------------------------------------------------- + + 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