public function testInvalidSelectorToString() { $json = <<<'JSON' { "class": "SocialEmbedRule", "selector" : "figure.op-social", "properties" : { "socialembed.url" : { "type" : "string", "selector" : "iframe", "attribute": "src" }, "socialembed.iframe" : { "type" : "children", "selector" : "iframe" } } } JSON; $properties = json_decode($json, true); $instant_article = InstantArticle::create(); $document = new \DOMDocument(); $node = $document->createElement('figcaption'); $rule = SocialEmbedRule::createFrom($properties); $warning = new InvalidSelector('field a and b', $instant_article, $node, $rule); $result = $warning->__toString(); $expected = 'Invalid selector for fields (field a and b). ' . 'The node being transformed was <figcaption> in the context of' . ' InstantArticle within the Rule SocialEmbedRule with these' . ' properties: { socialembed.url=StringGetter}'; $this->assertEquals($expected, $result); }
public function apply($transformer, $context_element, $node) { $h2 = H2::create(); if (Type::is($context_element, array(Header::getClassName(), Caption::getClassName()))) { $context_element->withSubTitle($h2); } elseif (Type::is($context_element, InstantArticle::getClassName())) { $context_element->addChild($h2); } if ($this->getProperty(Caption::POSITION_BELOW, $node)) { $h2->withPosition(Caption::POSITION_BELOW); } if ($this->getProperty(Caption::POSITION_CENTER, $node)) { $h2->withPosition(Caption::POSITION_CENTER); } if ($this->getProperty(Caption::POSITION_ABOVE, $node)) { $h2->withPosition(Caption::POSITION_ABOVE); } if ($this->getProperty(Caption::ALIGN_LEFT, $node)) { $h2->withTextAlignment(Caption::ALIGN_LEFT); } if ($this->getProperty(Caption::ALIGN_CENTER, $node)) { $h2->withTextAlignment(Caption::ALIGN_CENTER); } if ($this->getProperty(Caption::ALIGN_RIGHT, $node)) { $h2->withTextAlignment(Caption::ALIGN_RIGHT); } $transformer->transform($h2, $node); return $context_element; }
/** * This method navigates thru the tree structure and validates the article content. * * @param InstantArticle $article The article that will be checked. * @return array of string with the warnings raised during the check. */ public static function check($article) { Type::enforce($article, InstantArticle::getClassName()); $warnings = array(); self::getReport(array($article), $warnings); return $warnings; }
public function testInstantArticle() { $article = InstantArticle::create()->withCanonicalUrl('')->withHeader(Header::create())->addChild(Paragraph::create()->appendText('Some text to be within a paragraph for testing.'))->addChild(Paragraph::create())->addChild(Paragraph::create()->appendText(" \n \t "))->addChild(Image::create())->addChild(Image::create()->withURL(''))->addChild(SlideShow::create()->addImage(Image::create()->withURL('https://jpeg.org/images/jpegls-home.jpg'))->addImage(Image::create()))->addChild(Ad::create()->withSource('http://foo.com'))->addChild(Paragraph::create()->appendText('Other text to be within a second paragraph for testing.'))->addChild(Analytics::create())->withFooter(Footer::create()); $expected = '<!doctype html>' . '<html>' . '<head>' . '<link rel="canonical" href=""/>' . '<meta charset="utf-8"/>' . '<meta property="op:generator" content="facebook-instant-articles-sdk-php"/>' . '<meta property="op:generator:version" content="' . InstantArticle::CURRENT_VERSION . '"/>' . '<meta property="op:markup_version" content="v1.0"/>' . '</head>' . '<body>' . '<article>' . '<p>Some text to be within a paragraph for testing.</p>' . '<figure class="op-slideshow">' . '<figure>' . '<img src="https://jpeg.org/images/jpegls-home.jpg"/>' . '</figure>' . '</figure>' . '<figure class="op-ad">' . '<iframe src="http://foo.com"></iframe>' . '</figure>' . '<p>Other text to be within a second paragraph for testing.</p>' . '</article>' . '</body>' . '</html>'; $result = $article->render(); $this->assertEquals($expected, $result); $warnings = InstantArticleValidator::check($article); $this->assertEquals(9, count($warnings)); }
public function apply($transformer, $context, $node) { $interactive = Interactive::create(); if (Type::is($context, InstantArticle::getClassName())) { $instant_article = $context; } elseif ($transformer->getInstantArticle()) { $instant_article = $transformer->getInstantArticle(); $context->disableEmptyValidation(); $context = Paragraph::create(); $context->disableEmptyValidation(); } else { $transformer->addWarning(new NoRootInstantArticleFoundWarning(null, $node)); return $context; } // Builds the interactive $iframe = $this->getProperty(self::PROPERTY_IFRAME, $node); $url = $this->getProperty(self::PROPERTY_URL, $node); if ($iframe) { $interactive->withHTML($iframe); } if ($url) { $interactive->withSource($url); } if ($iframe || $url) { $instant_article->addChild($interactive); if ($instant_article !== $context) { $instant_article->addChild($context); } } else { $transformer->addWarning(new InvalidSelector(self::PROPERTY_IFRAME, $instant_article, $node, $this)); } if ($this->getProperty(self::PROPERTY_WIDTH_COLUMN_WIDTH, $node)) { $interactive->withMargin(Interactive::COLUMN_WIDTH); } else { $interactive->withMargin(Interactive::NO_MARGIN); } $width = $this->getProperty(self::PROPERTY_WIDTH, $node); if ($width) { $interactive->withWidth($width); } $height = $this->getProperty(self::PROPERTY_HEIGHT, $node); if ($height) { $interactive->withHeight($height); } $suppress_warnings = $transformer->suppress_warnings; $transformer->suppress_warnings = true; $transformer->transform($interactive, $node); $transformer->suppress_warnings = $suppress_warnings; return $context; }
public function testTransformPullquote() { $transformer_rules = <<<'JSON' { "rules" : [ { "class": "TextNodeRule" }, { "class": "ItalicRule", "selector": "em" }, { "class": "ParagraphRule", "selector": "p" }, { "class": "PassThroughRule", "selector": "div.field-quote > p" }, { "class": "PassThroughRule", "selector" : "div.field-quote" }, { "class" : "PullquoteRule", "selector" : "blockquote.pull-quote" }, { "class" : "PullquoteCiteRule", "selector" : "div.field-quote-author" } ] } JSON; $html = '<blockquote class="pull-quote">' . '<div class="field-quote">' . '<p>Here is a fancy pull quote for the <em>world</em> to see it all.</p>' . '</div>' . '<div class="field-quote-author">Matthew Oliveira</div>' . '</blockquote>'; $expected = "<aside>Here is a fancy pull quote for the <i>world</i> to see it all." . "<cite>Matthew Oliveira</cite>" . "</aside>\n"; $instant_article = InstantArticle::create(); $transformer = new Transformer(); $transformer->loadRules($transformer_rules); $document = new \DOMDocument(); $document->loadXML($html); $transformer->transform($instant_article, $document); $pullquote = $instant_article->getChildren()[0]; $result = $pullquote->render('', true) . "\n"; $this->assertEquals($expected, $result); }
/** * @param string|DOMDocument $document The document html of an Instant Article * * @return InstantArticle filled element that was parsed from the DOMDocument parameter */ public function parse($content) { if (Type::is($content, Type::STRING)) { libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadHTML($content); libxml_use_internal_errors(false); } else { $document = $content; } $json_file = file_get_contents(__DIR__ . '/instant-articles-rules.json'); $instant_article = InstantArticle::create(); $transformer = new Transformer(); $transformer->loadRules($json_file); $transformer->transform($instant_article, $document); return $instant_article; }
public function apply($transformer, $context, $node) { $image = Image::create(); if (Type::is($context, InstantArticle::getClassName())) { $instant_article = $context; } elseif ($transformer->getInstantArticle()) { $instant_article = $transformer->getInstantArticle(); $context->disableEmptyValidation(); $context = Paragraph::create(); $context->disableEmptyValidation(); } else { $transformer->addWarning(new NoRootInstantArticleFoundWarning(null, $node)); return $context; } // Builds the image $url = $this->getProperty(self::PROPERTY_IMAGE_URL, $node); if ($url) { $image->withURL($url); $instant_article->addChild($image); if ($instant_article !== $context) { $instant_article->addChild($context); } } else { $transformer->addWarning(new InvalidSelector(self::PROPERTY_IMAGE_URL, $instant_article, $node, $this)); } if ($this->getProperty(Image::ASPECT_FIT, $node)) { $image->withPresentation(Image::ASPECT_FIT); } elseif ($this->getProperty(Image::ASPECT_FIT_ONLY, $node)) { $image->withPresentation(Image::ASPECT_FIT_ONLY); } elseif ($this->getProperty(Image::FULLSCREEN, $node)) { $image->withPresentation(Image::FULLSCREEN); } elseif ($this->getProperty(Image::NON_INTERACTIVE, $node)) { $image->withPresentation(Image::NON_INTERACTIVE); } if ($this->getProperty(self::PROPERTY_LIKE, $node)) { $image->enableLike(); } if ($this->getProperty(self::PROPERTY_COMMENTS, $node)) { $image->enableComments(); } $suppress_warnings = $transformer->suppress_warnings; $transformer->suppress_warnings = true; $transformer->transform($image, $node); $transformer->suppress_warnings = $suppress_warnings; return $context; }
public function testSelfTransformerContent() { $json_file = file_get_contents(__DIR__ . '/simple-rules.json'); $instant_article = InstantArticle::create(); $transformer = new Transformer(); $transformer->loadRules($json_file); $html_file = file_get_contents(__DIR__ . '/simple.html'); libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadHTML($html_file); libxml_use_internal_errors(false); $transformer->transform($instant_article, $document); $instant_article->addMetaProperty('op:generator:version', '1.0.0'); $instant_article->addMetaProperty('op:generator:transformer:version', '1.0.0'); $result = $instant_article->render('', true) . "\n"; $expected = file_get_contents(__DIR__ . '/simple-ia.html'); //var_dump($result); // print_r($warnings); $this->assertEquals($expected, $result); }
public function testTransformerCustomHTML() { $json_file = file_get_contents(__DIR__ . '/custom-html-rules.json'); $instant_article = InstantArticle::create(); $transformer = new Transformer(); $transformer->loadRules($json_file); $html_file = file_get_contents(__DIR__ . '/custom.html'); libxml_use_internal_errors(true); $document = new \DOMDocument(); $document->loadHTML($html_file); libxml_use_internal_errors(false); $instant_article->withCanonicalURL('http://localhost/article')->withHeader(Header::create()->withTitle('Peace on <b>earth</b>')->addAuthor(Author::create()->withName('bill'))->withPublishTime(Time::create(Time::PUBLISHED)->withDatetime(\DateTime::createFromFormat('j-M-Y G:i:s', '12-Apr-2016 19:46:51')))); $transformer->transform($instant_article, $document); $instant_article->addMetaProperty('op:generator:version', '1.0.0'); $instant_article->addMetaProperty('op:generator:transformer:version', '1.0.0'); $result = $instant_article->render('', true) . "\n"; $expected = file_get_contents(__DIR__ . '/custom-html-ia.xml'); $this->assertEquals($expected, $result); // there must be 3 warnings related to <img> inside <li> that is not supported by IA $this->assertEquals(3, count($transformer->getWarnings())); }
public function getContextClass() { return InstantArticle::getClassName(); }
public function testSelfTransformerNonUTF8Content() { $json_file = file_get_contents('src/Facebook/InstantArticles/Parser/instant-articles-rules.json'); $instant_article = InstantArticle::create(); $transformer = new Transformer(); $transformer->loadRules($json_file); $html_file = file_get_contents(__DIR__ . '/instant-article-example-nonutf8.html'); $transformer->transformString($instant_article, $html_file, 'euc-jp'); $instant_article->withCanonicalURL('http://foo.com/article.html'); $instant_article->addMetaProperty('op:generator:version', '1.0.0'); $instant_article->addMetaProperty('op:generator:transformer:version', '1.0.0'); $result = $instant_article->render('', true) . "\n"; // some fragments are written as html entities even after transformed so // noralize all strings to html entities and compare them. $this->assertEquals(mb_convert_encoding($html_file, 'HTML-ENTITIES', 'euc-jp'), mb_convert_encoding($result, 'HTML-ENTITIES', 'utf-8')); }
public function testIsNotInSetException() { $this->setExpectedException('InvalidArgumentException'); Type::enforce(Caption::create(), [InstantArticle::getClassName(), Video::getClassName(), Image::getClassName()]); }
protected function setUp() { $this->facebook = $this->getMockBuilder('Facebook\\Facebook')->disableOriginalConstructor()->getMock(); $this->client = new Client($this->facebook, "PAGE_ID", false); $this->article = InstantArticle::create()->addChild(Paragraph::create()->appendText('Test')); }
/** * Render post * * @since 0.1 * @return InstantArticle */ public function to_instant_article() { /** * Fires before the instant article is rendered. * * @since 0.1 * @param Instant_Article_Post $instant_article_post The instant article post. */ do_action('instant_articles_before_transform_post', $this); // Get time zone configured in WordPress. Default to UTC if no time zone configured. $date_time_zone = get_option('timezone_string') ? new DateTimeZone(get_option('timezone_string')) : new DateTimeZone('UTC'); // Initialize transformer $file_path = plugin_dir_path(__FILE__) . 'rules-configuration.json'; $configuration = file_get_contents($file_path); $transformer = new Transformer(); $this->transformer = $transformer; $transformer->loadRules($configuration); $transformer = apply_filters('instant_articles_transformer_rules_loaded', $transformer); $settings_publishing = Instant_Articles_Option_Publishing::get_option_decoded(); if (isset($settings_publishing['custom_rules_enabled']) && !empty($settings_publishing['custom_rules_enabled']) && isset($settings_publishing['custom_rules']) && !empty($settings_publishing['custom_rules'])) { $transformer->loadRules($settings_publishing['custom_rules']); } $transformer = apply_filters('instant_articles_transformer_custom_rules_loaded', $transformer); $blog_charset = get_option('blog_charset'); $header = Header::create()->withPublishTime(Time::create(Time::PUBLISHED)->withDatetime(new DateTime($this->_post->post_date, $date_time_zone)))->withModifyTime(Time::create(Time::MODIFIED)->withDatetime(new DateTime($this->_post->post_modified, $date_time_zone))); $title = $this->get_the_title(); if ($title) { $document = new DOMDocument(); libxml_use_internal_errors(true); $document->loadHTML('<?xml encoding="' . $blog_charset . '" ?><h1>' . $title . '</h1>'); libxml_use_internal_errors(false); $transformer->transform($header, $document); } if ($this->has_subtitle()) { $header->withSubTitle($this->get_the_subtitle()); } $authors = $this->get_the_authors(); foreach ($authors as $author) { $author_obj = Author::create(); if ($author->display_name) { $author_obj->withName($author->display_name); } if ($author->bio) { $author_obj->withDescription($author->bio); } if ($author->user_url) { $author_obj->withURL($author->user_url); } $header->addAuthor($author_obj); } $kicker = $this->get_the_kicker(); if ($kicker) { $header->withKicker($kicker); } $cover = $this->get_cover_media(); if ($cover->getUrl()) { $header->withCover($cover); } $this->instant_article = InstantArticle::create()->withCanonicalUrl($this->get_canonical_url())->withHeader($header)->addMetaProperty('op:generator:application', 'facebook-instant-articles-wp')->addMetaProperty('op:generator:application:version', IA_PLUGIN_VERSION); $settings_style = Instant_Articles_Option_Styles::get_option_decoded(); if (isset($settings_style['article_style']) && !empty($settings_style['article_style'])) { $this->instant_article->withStyle($settings_style['article_style']); } else { $this->instant_article->withStyle('default'); } $libxml_previous_state = libxml_use_internal_errors(true); $document = new DOMDocument('1.0', get_option('blog_charset')); $content = $this->get_the_content(); // DOMDocument isn’t handling encodings too well, so let’s help it a little. if (function_exists('mb_convert_encoding')) { $content = mb_convert_encoding($content, 'HTML-ENTITIES', get_option('blog_charset')); } else { $content = htmlspecialchars_decode(utf8_decode(htmlentities($content, ENT_COMPAT, 'utf-8', false))); } $result = $document->loadHTML('<!doctype html><html><body>' . $content . '</body></html>'); // We need to make sure that scripts use absolute URLs and not relative URLs. $scripts = $document->getElementsByTagName('script'); if (!empty($scripts)) { foreach ($scripts as $script) { $src = $script->getAttribute('src'); $explode_src = parse_url($src); if (is_array($explode_src) && empty($explode_src['scheme']) && !empty($explode_src['host']) && !empty($explode_src['path'])) { $src = 'https://' . $explode_src['host'] . $explode_src['path']; } $script->setAttribute('src', $src); } } libxml_clear_errors(); libxml_use_internal_errors($libxml_previous_state); $document = apply_filters('instant_articles_parsed_document', $document); if ($result) { $transformer->transform($this->instant_article, $document); } $this->add_ads_from_settings(); $this->add_analytics_from_settings(); $this->instant_article = apply_filters('instant_articles_transformed_element', $this->instant_article); /** * Fires after the instant article is rendered. * * @since 0.1 * @param Instant_Article_Post $instant_article_post The instant article post. */ do_action('instant_articles_after_transform_post', $this); return $this->instant_article; }
/** * @param InstantArticle $context * @param \DOMNode $node * * @return mixed */ public function transform($context, $node) { if (Type::is($context, InstantArticle::getClassName())) { $context->addMetaProperty('op:generator:transformer', 'facebook-instant-articles-sdk-php'); $context->addMetaProperty('op:generator:transformer:version', InstantArticle::CURRENT_VERSION); $this->instantArticle = $context; } $log = \Logger::getLogger('facebook-instantarticles-transformer'); if (!$node) { $e = new \Exception(); $log->error('Transformer::transform($context, $node) requires $node' . ' to be a valid one. Check on the stacktrace if this is ' . 'some nested transform operation and fix the selector.', $e->getTraceAsString()); } $current_context = $context; if ($node->hasChildNodes()) { foreach ($node->childNodes as $child) { if (self::isProcessed($child)) { continue; } $matched = false; $log->debug("==========================="); $log->debug($child->ownerDocument->saveHtml($child)); // Get all classes and interfaces this context extends/implements $contextClassNames = self::getAllClassTypes($context->getClassName()); // Look for rules applying to any of them as context $matchingContextRules = []; foreach ($contextClassNames as $contextClassName) { if (isset($this->rules[$contextClassName])) { // Use array union (+) instead of merge to preserve // indexes (as they represent the order of insertion) $matchingContextRules = $matchingContextRules + $this->rules[$contextClassName]; } } // Sort by insertion order ksort($matchingContextRules); // Process in reverse order $matchingContextRules = array_reverse($matchingContextRules); foreach ($matchingContextRules as $rule) { // We know context was matched, now check if it matches the node if ($rule->matchesNode($child)) { $current_context = $rule->apply($this, $current_context, $child); $matched = true; // Just a single rule for each node, so move on break; } } if (!$matched && !($child->nodeName === '#text' && trim($child->textContent) === '') && !($child->nodeName === '#comment') && !($child->nodeName === 'html' && Type::is($child, 'DOMDocumentType')) && !($child->nodeName === 'xml' && Type::is($child, 'DOMProcessingInstruction')) && !$this->suppress_warnings) { $tag_content = $child->ownerDocument->saveXML($child); $tag_trimmed = trim($tag_content); if (!empty($tag_trimmed)) { $log->debug('context class: ' . get_class($context)); $log->debug('node name: ' . $child->nodeName); $log->debug("CONTENT NOT MATCHED: \n" . $tag_content); } else { $log->debug('empty content ignored'); } $this->addWarning(new UnrecognizedElement($current_context, $child)); } } } return $context; }
/** * Render post * * @since 0.1 * @return InstantArticle */ public function to_instant_article() { /** * Fires before the instant article is rendered. * * @since 0.1 * @param Instant_Article_Post $instant_article_post The instant article post. */ do_action('instant_articles_before_transform_post', $this); // Get time zone configured in WordPress. Default to UTC if no time zone configured. $date_time_zone = get_option('timezone_string') ? new DateTimeZone(get_option('timezone_string')) : new DateTimeZone('UTC'); // Initialize transformer $file_path = plugin_dir_path(__FILE__) . 'rules-configuration.json'; $configuration = file_get_contents($file_path); $transformer = new Transformer(); $this->transformer = $transformer; $transformer->loadRules($configuration); $transformer = apply_filters('instant_articles_transformer_rules_loaded', $transformer); $settings_publishing = Instant_Articles_Option_Publishing::get_option_decoded(); if (isset($settings_publishing['custom_rules_enabled']) && !empty($settings_publishing['custom_rules_enabled']) && isset($settings_publishing['custom_rules']) && !empty($settings_publishing['custom_rules'])) { $transformer->loadRules($settings_publishing['custom_rules']); } $transformer = apply_filters('instant_articles_transformer_custom_rules_loaded', $transformer); $blog_charset = get_option('blog_charset'); $header = Header::create()->withPublishTime(Time::create(Time::PUBLISHED)->withDatetime(new DateTime($this->_post->post_date, $date_time_zone)))->withModifyTime(Time::create(Time::MODIFIED)->withDatetime(new DateTime($this->_post->post_modified, $date_time_zone))); $title = $this->get_the_title(); if ($title) { $document = new DOMDocument(); libxml_use_internal_errors(true); $document->loadHTML('<?xml encoding="' . $blog_charset . '" ?><h1>' . $title . '</h1>'); libxml_use_internal_errors(false); $transformer->transform($header, $document); } if ($this->has_subtitle()) { $header->withSubTitle($this->get_the_subtitle()); } $authors = $this->get_the_authors(); foreach ($authors as $author) { $author_obj = Author::create(); if ($author->display_name) { $author_obj->withName($author->display_name); } if ($author->bio) { $author_obj->withDescription($author->bio); } if ($author->user_url) { $author_obj->withURL($author->user_url); } $header->addAuthor($author_obj); } $kicker = $this->get_the_kicker(); if ($kicker) { $header->withKicker($kicker); } $cover = $this->get_cover_media(); if ($cover->getUrl()) { $header->withCover($cover); } $this->instant_article = InstantArticle::create()->withCanonicalUrl($this->get_canonical_url())->withHeader($header)->addMetaProperty('op:generator:application', 'facebook-instant-articles-wp')->addMetaProperty('op:generator:application:version', IA_PLUGIN_VERSION); $settings_style = Instant_Articles_Option_Styles::get_option_decoded(); if (isset($settings_style['article_style']) && !empty($settings_style['article_style'])) { $this->instant_article->withStyle($settings_style['article_style']); } else { $this->instant_article->withStyle('default'); } $transformer->transformString($this->instant_article, $this->get_the_content(), get_option('blog_charset')); $this->add_ads_from_settings(); $this->add_analytics_from_settings(); $this->instant_article = apply_filters('instant_articles_transformed_element', $this->instant_article); /** * Fires after the instant article is rendered. * * @since 0.1 * @param Instant_Article_Post $instant_article_post The instant article post. */ do_action('instant_articles_after_transform_post', $this); return $this->instant_article; }