├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── examples ├── example-override-styling.php ├── example-quick-start.php ├── example-simple-styling.php ├── instant-article-example.html └── styles │ ├── gray.style.json │ └── style-got-from-somewhereelse.json ├── phpcs.xml ├── phpunit.xml ├── src └── Facebook │ └── InstantArticles │ ├── AMP │ ├── AMPArticle.php │ ├── AMPCaption.php │ ├── AMPContext.php │ ├── AMPCover.php │ ├── AMPCoverImage.php │ ├── AMPHeader.php │ ├── AMPImage.php │ └── configuration │ │ ├── default-amp.style.json │ │ └── global.amp.css │ └── Utils │ ├── CSSBuilder.php │ ├── CallbackHook.php │ ├── Hook.php │ ├── Observer.php │ └── Warning.php └── tests └── Facebook └── InstantArticles ├── AMP ├── AMPArticleTest.php ├── AMPCaptionTest.php ├── AMPContextTest.php ├── AMPCoverImageTest.php ├── AMPHeaderTest.php ├── articles │ ├── amp-converted.html │ ├── amp-example.html │ ├── media-cache │ │ ├── fail1.jpg │ │ ├── fb_icon_325x325.png │ │ ├── heart-rate-age-300x182.gif │ │ └── timelapse-final-4x3.mp4 │ ├── test1-amp-converted.html │ ├── test1-instant-article.html │ ├── test2-amp-converted.html │ ├── test2-instant-article.html │ ├── test3-amp-converted.html │ ├── test3-instant-article.html │ ├── test4-amp-converted.html │ ├── test4-instant-article.html │ ├── test5-amp-converted.html │ ├── test5-instant-article.html │ ├── tutorial-amp-converted.html │ └── tutorial-instant-article.html ├── default.amp-custom.css ├── default.style.json └── wod-gray.style.json └── Utils ├── CSSBuilderTest.php ├── FileUtilsPHPUnitTestCase.php ├── Greeting.php ├── HookTest.php └── ObserverTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | *.ignored 4 | **/IgnoredTest.php 5 | .DS_Store 6 | .vscode 7 | **/ignored-* 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.5 6 | env: WITH_CS=true 7 | - php: 5.6 8 | - php: 7.0 9 | - php: 7.1 10 | 11 | dist: trusty 12 | sudo: false 13 | 14 | cache: 15 | directories: 16 | - $HOME/.composer/cache 17 | 18 | before_install: 19 | - if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi 20 | - travis_retry composer self-update 21 | - composer validate 22 | - if [[ -n "$GITHUB_TOKEN" ]]; then composer config github-oauth.github.com $GITHUB_TOKEN; fi 23 | 24 | install: 25 | - travis_retry composer install --no-interaction --prefer-dist 26 | 27 | script: 28 | - vendor/bin/phpunit 29 | - if [[ "$WITH_CS" == "true" ]]; then vendor/bin/phpcs --standard=phpcs.xml -p; fi 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.facebook.com/codeofconduct) so that you can understand what actions will and will not be tolerated. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Facebook Instant Articles SDK Extensions in PHP 2 | We want to make contributing to this project as easy and transparent as possible. 3 | 4 | We accept contributions via pull requests on [GitHub](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php). 5 | 6 | ## Code of Conduct 7 | The code of conduct is described in [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) 8 | 9 | ## Pull Requests 10 | - **Sign the CLA** - In order to accept your pull request, we need you to submit a [Contributor License Agreement](https://code.facebook.com/cla). You only need to do this once to work on any of Facebook's open source projects. 11 | 12 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to run [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) as you code. 13 | 14 | - **Add tests** - If you've added code that can be tested, add tests. 15 | 16 | - **Document any change in behaviour** - Make sure the README file is updated in your PR. Include any notes for documentation items which needs to be updated on the main [docs on Facebook's Developer site](https://developers.facebook.com/docs/instant-articles/sdk/). 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create topic branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 25 | 26 | - **Ensure tests pass!** - Please [run the tests](https://github.com/facebook/facebook-instant-articles-sdk-extensions-php#testing-and-developing) before submitting your pull request, and make sure they pass. We won't accept a patch until all tests pass. 27 | 28 | - **Ensure no coding standards violations** - Please run [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) using the PSR-2 standard before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails. 29 | 30 | 31 | ## Issues 32 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 33 | 34 | 35 | ## Running Tests 36 | 37 | ``` bash 38 | $ ./vendor/bin/phpunit 39 | ``` 40 | 41 | When doing a pull request, consider if this diff has a testcase that was covered in a wrong way or if it needs a new test case. 42 | 43 | 44 | ## Running PHP Code Sniffer 45 | 46 | Run Code Sniffer against the `src/` and `tests/` directories. 47 | 48 | ``` bash 49 | $ vendor/bin/phpcs --standard=phpcs.xml -p 50 | ``` 51 | 52 | Give a try for the autofixer for code style 53 | 54 | ``` bash 55 | $ vendor/bin/phpcbf --standard-phpcs.xml -p 56 | ``` 57 | **Happy coding**! 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-present, Facebook, Inc. All rights reserved. 2 | 3 | You are hereby granted a non-exclusive, worldwide, royalty-free license to 4 | use, copy, modify, and distribute this software in source code or binary 5 | form for use in connection with the web services and APIs provided by 6 | Facebook. 7 | 8 | As with any software that integrates with the Facebook platform, your use 9 | of this software is subject to the Facebook Developer Principles and 10 | Policies [http://developers.facebook.com/policy/]. This copyright notice 11 | shall be included in all copies or substantial portions of the software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Facebook Instant Articles SDK Extensions in PHP # 2 | 3 | [![Build Status](https://travis-ci.org/facebook/facebook-instant-articles-sdk-extensions-in-php.svg?branch=master)](https://travis-ci.org/facebook/facebook-instant-articles-sdk-extensions-in-php) 4 | [![Latest Stable Version](https://poser.pugx.org/facebook/facebook-instant-articles-sdk-extensions-in-php/v/stable)](https://packagist.org/packages/facebook/facebook-instant-articles-sdk-extensions-in-php) 5 | 6 | The Facebook Instant Articles SDK Extensions in PHP provides a native PHP interface for converting valid Instant Articles into AMP. This gives developers the ability to have AMP content right after getting his own Instant Article markup format ready. 7 | 8 | The Extension package consists of: 9 | - **Environment**: PHP >= 5.4 10 | - **Dependencies**: It relies solely on the [Instant Articles SDK](https://github.com/Facebook/facebook-instant-articles-sdk-php) and its dependencies to get the Instant Article markup format available into the Elements object tree structure. It also depends on [Composer](https://getcomposer.org/) dependency manager. 11 | - **AMP**: The AMP transformation was based on the current implementation and definition from [AMP project](https://www.ampproject.org/). 12 | 13 | ## Quick Start 14 | 15 | ```sh 16 | $ composer require facebook/facebook-instant-articles-sdk-extensions-in-php 17 | ``` 18 | 19 | After the installation, you can include the auto loader script in your source with: 20 | 21 | ```PHP 22 | require_once('vendor/autoload.php'); 23 | ``` 24 | 25 | Also be sure to check [the quick start example](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/blob/master/examples/example-quick-start.php). 26 | 27 | ## Official Documentation 28 | 29 | You can find examples on how to use the different components of this SDK to integrate with your CMS in the [Quick Start Guide](https://developers.facebook.com/docs/instant-articles/other-formats/#quickstart) of the [documentation](https://developers.facebook.com/docs/instant-articles/other-formats/). 30 | 31 | ## Contributing 32 | 33 | Clone the repository 34 | ```sh 35 | $ git clone https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php.git 36 | ``` 37 | 38 | [Composer](https://getcomposer.org/) is a prerequisite for testing and developing. [Install composer globally](https://getcomposer.org/doc/00-intro.md#globally), then install project dependencies by running this command in the project's root directory: 39 | 40 | ```sh 41 | $ composer install 42 | ``` 43 | 44 | To run the tests: 45 | 46 | ```sh 47 | $ composer test 48 | ``` 49 | 50 | To fix and check for coding style issues: 51 | 52 | ```sh 53 | $ composer cs 54 | ``` 55 | 56 | Extra lazy? Run 57 | 58 | ```sh 59 | $ composer all 60 | ``` 61 | 62 | to fix and check for coding style issues, and run the tests. 63 | 64 | If you change structure, paths, namespaces, etc., make sure you run the [autoload generator](https://getcomposer.org/doc/03-cli.md#dump-autoload): 65 | ```sh 66 | $ composer dump-autoload 67 | ``` 68 | 69 | ___ 70 | **For us to accept contributions you will have to first sign the [Contributor License Agreement](https://code.facebook.com/cla). Please see [CONTRIBUTING](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/blob/master/CONTRIBUTING.md) for details.** 71 | ___ 72 | 73 | ## Troubleshooting 74 | 75 | If you are encountering problems, the following tips may help in troubleshooting issues: 76 | 77 | - If your images are having dimension/aspect ratio problems, please check the [the quick start example](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/blob/master/examples/example-quick-start.php) for more information. 78 | - At the moment, we have no way to determine a video's width and height, you need to explicitly pass that information via properties, see how to do it [here](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/blob/master/examples/example-quick-start.php). 79 | 80 | ### Filing an issue 81 | 82 | - Be sure you've looked for the similar issue into the [Issues list](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/issues) 83 | 84 | - Inform the Canonical URL of your Instant Article being converted 85 | 86 | - Inform the [exported JSON from the style editor](https://developers.facebook.com/docs/instant-articles/other-formats#style) 87 | 88 | Issue template: 89 | ``` 90 | # Issue Data 91 | Instant Article Canonical URL: `http://yourdomain.com/path/article.html` 92 | Exported Style JSON: `{...}` 93 | Page ID: `12345` 94 | 95 | # Problem noticed 96 | 1. Image XYZ missing 97 | 98 | # Expected result 99 | 1. The Image XYZ should be present. 100 | ``` 101 | 102 | ## License 103 | 104 | Please see the [license file](https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/blob/master/LICENSE) for more information. 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facebook/facebook-instant-articles-sdk-extensions-in-php", 3 | "description": "Facebook Instant Articles SDK Extensions in PHP to transform Instant Articles markup file into AMP", 4 | "keywords": ["facebook", "sdk", "instant", "articles", "instantarticles", "amp", "extensions"], 5 | "type": "library", 6 | "homepage": "https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php", 7 | "license": "proprietary", 8 | "authors": [{ 9 | "name": "Facebook", 10 | "homepage": "https://github.com/facebook/facebook-instant-articles-sdk-extensions-in-php/contributors" 11 | }], 12 | "config": { 13 | "sort-packages": true 14 | }, 15 | "require": { 16 | "doctrine/instantiator": "<=1.0.5", 17 | "php": "^5.4 || ^7.0", 18 | "facebook/facebook-instant-articles-sdk-php": "^1.8.3" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^4.8", 22 | "squizlabs/php_codesniffer": "^3.0.0", 23 | "phpdocumentor/reflection-docblock": "^2.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Facebook\\InstantArticles\\": "src/Facebook/InstantArticles/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Facebook\\InstantArticles\\": "tests/Facebook/InstantArticles/" 33 | } 34 | }, 35 | "scripts": { 36 | "all": [ 37 | "@cs", 38 | "@test" 39 | ], 40 | "cs": [ 41 | "composer install", 42 | "phpcbf --standard=phpcs.xml -p || phpcs --standard=phpcs.xml -p" 43 | ], 44 | "test": [ 45 | "composer install", 46 | "phpunit" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/example-override-styling.php: -------------------------------------------------------------------------------- 1 | json_decode($styleGotFromSomewhereElse, true) 23 | ); 24 | 25 | // Converts it into AMP 26 | $amp_string = AMPArticle::create($instant_article_string, $properties)->render(); 27 | 28 | print($amp_string); 29 | -------------------------------------------------------------------------------- /examples/example-quick-start.php: -------------------------------------------------------------------------------- 1 | or tags to the following option. 48 | // 49 | // See also the AMP specs for these tags: 50 | // - https://www.ampproject.org/docs/reference/components/amp-analytics 51 | // - https://www.ampproject.org/docs/reference/components/amp-pixel 52 | $properties[AMPArticle::ANALYTICS_KEY] = array( 53 | '', 54 | '' 55 | ); 56 | 57 | // Converts it into AMP 58 | $amp_string = 59 | AMPArticle::create($instant_article_string, $properties)->render(); 60 | 61 | print($amp_string); 62 | -------------------------------------------------------------------------------- /examples/example-simple-styling.php: -------------------------------------------------------------------------------- 1 | 'en-US', // You can set the language your article have 18 | 'styles-folder' => __DIR__.'/styles' // Where the styles are stored 19 | ); 20 | 21 | /* 22 | As the name of the style used within the Instant article refers to "gray", the 23 | file within directory /styles will be the /styles/gray.style.json. 24 | */ 25 | 26 | // Converts it into AMP 27 | $amp_string = AMPArticle::create($instant_article_string, $properties)->render(); 28 | 29 | print($amp_string); 30 | -------------------------------------------------------------------------------- /examples/instant-article-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 |

Article Title

15 |

Article Subtitle

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | Brandon Diamond 26 | Brandon is an avid zombie hunter. 27 |
28 |
29 | TR Vishwanath 30 | Vish is a scholar and a gentleman. 31 |
32 | 33 | 34 |
35 | 36 |
This image is amazing
37 |
38 | 39 | 40 |

41 | This is a kicker 42 |

43 | 44 |
45 | 46 | 47 | 48 | 49 |

Article content

50 | 51 | 52 |
53 | 56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 | 64 |
65 | 66 |
Like it!
67 |
68 | 69 | 70 |
71 | 72 |
This is crazy
73 |
74 | 75 | 76 |
77 | 78 |
79 | 80 |
81 | 82 | 83 | 84 | 85 | Legal notes 86 |
87 |
88 | 89 | 90 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | . 4 | vendor/ 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | ./src 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/AMP/AMPCaption.php: -------------------------------------------------------------------------------- 1 | caption = $caption; 48 | $this->context = $context; 49 | $this->ampTag = $ampCaptionedElement; 50 | } 51 | 52 | public static function create($caption, $context, $ampCaptionedElement) 53 | { 54 | return new AMPCaption($caption, $context, $ampCaptionedElement); 55 | } 56 | 57 | private function genContainer() 58 | { 59 | $this->container = $this->context->createElement('figure', null, 'figure'); 60 | } 61 | 62 | private function appendCaptioned() 63 | { 64 | $fontSize = $this->caption->getFontSize(); 65 | $cssClass = 'figcaption-' . ($fontSize ? $fontSize : 'small'); 66 | 67 | $this->ampCaption = $this->context->createElement('figcaption', null, $cssClass); 68 | 69 | $position = $this->caption->getPosition(); 70 | if (!$position) { 71 | $position = Caption::POSITION_BELOW; 72 | } 73 | 74 | if ($position === Caption::POSITION_BELOW) { 75 | $this->container->appendChild($this->ampTag); 76 | $this->container->appendChild($this->ampCaption); 77 | } else { 78 | $this->container->appendChild($this->ampCaption); 79 | $this->container->appendChild($this->ampTag); 80 | } 81 | } 82 | 83 | private function genTitle() 84 | { 85 | // Title 86 | $title = $this->caption->getTitle(); 87 | if ($title) { 88 | $ampTitle = $this->context->createElement('h1', $this->ampCaption); 89 | $ampTitleText = $title->textToDOMDocumentFragment($this->context->getDocument()); 90 | $ampTitle->appendChild($ampTitleText); 91 | } 92 | } 93 | 94 | private function genSubtitle() 95 | { 96 | // SubTitle 97 | $subTitle = $this->caption->getSubTitle(); 98 | if ($subTitle) { 99 | $ampSubTitle = $this->context->createElement('h2', $this->ampCaption); 100 | $ampSubTitleText = $subTitle->textToDOMDocumentFragment($this->context->getDocument()); 101 | $ampSubTitle->appendChild($ampSubTitleText); 102 | } 103 | } 104 | 105 | private function genText() 106 | { 107 | // Text 108 | $ampText = $this->caption->textToDOMDocumentFragment($this->context->getDocument()); 109 | $this->ampCaption->appendChild($ampText); 110 | } 111 | 112 | private function genCredit() 113 | { 114 | // Credit 115 | $credit = $this->caption->getCredit(); 116 | if ($credit) { 117 | $ampCredit = $this->context->createElement('cite', $this->ampCaption); 118 | $ampCreditText = $credit->textToDOMDocumentFragment($this->context->getDocument()); 119 | $ampCredit->appendChild($ampCreditText); 120 | } 121 | } 122 | 123 | private function applyStyleClasses() 124 | { 125 | $ampCSSClasses = array(); 126 | $ampCSSClasses[] = $this->context->buildCssClass('figcaption'); 127 | 128 | if ($this->caption->getFontSize()) { 129 | $ampCSSClasses[] = $this->context->buildCssClass($this->caption->getFontSize()); 130 | } else { 131 | $ampCSSClasses[] = $this->context->buildCssClass(Caption::SIZE_SMALL); 132 | } 133 | if ($this->caption->getTextAlignment()) { 134 | $ampCSSClasses[] = $this->context->buildCssClass($this->caption->getTextAlignment()); 135 | } 136 | if ($this->caption->getPosition()) { 137 | $ampCSSClasses[] = $this->context->buildCssClass($this->caption->getPosition()); 138 | } 139 | if ($this->caption->getVerticalAlignment()) { 140 | $ampCSSClasses[] = $this->context->buildCssClass($this->caption->getVerticalAlignment()); 141 | } 142 | 143 | $this->ampCaption->setAttribute('class', implode(' ', $ampCSSClasses)); 144 | } 145 | 146 | public function build() 147 | { 148 | $this->genContainer(); 149 | $this->appendCaptioned(); 150 | $this->genTitle(); 151 | $this->genSubtitle(); 152 | $this->genText(); 153 | $this->genCredit(); 154 | $this->applyStyleClasses(); 155 | 156 | return $this->container; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/AMP/AMPCover.php: -------------------------------------------------------------------------------- 1 | context = $context; 40 | $this->coverElement = $coverElement; 41 | } 42 | 43 | private function genContainer() 44 | { 45 | if (Type::is($this->coverElement, Image::getClassName())) { 46 | $this->coverTag = AMPCoverImage::create($this->coverElement, $this->context, 'cover-image')->build(); 47 | } else if (Type::is($this->coverElement, Slideshow::getClassName())) { 48 | //return $this->buildSlideshow($this->coverElement, $context, 'cover-slideshow'); 49 | } else if (Type::is($this->coverElement, Video::getClassName())) { 50 | //return $this->buildVideo($this->coverElement, $context, 'cover-video'); 51 | } 52 | } 53 | 54 | public function build() 55 | { 56 | $this->genContainer(); 57 | return $this->coverTag; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/AMP/AMPCoverImage.php: -------------------------------------------------------------------------------- 1 | image = $image; 25 | $this->context = $context; 26 | $this->cssClass = $cssClass; 27 | } 28 | 29 | public static function create($image, $context, $cssClass) 30 | { 31 | return new self($image, $context, $cssClass); 32 | } 33 | 34 | private function genContainer() 35 | { 36 | $this->containerTag = $this->context->createElement('div', null, $this->cssClass); 37 | } 38 | 39 | private function genCaptionContainer() 40 | { 41 | $caption = $this->image->getCaption(); 42 | if ($caption) { 43 | $this->ampImgTag = 44 | AMPCaption::create($caption, $this->context, $this->ampImgTag)->build(); 45 | } 46 | } 47 | 48 | private function genAmpImage() 49 | { 50 | $this->ampImgTag = $this->context->createElement('amp-img', null, 'header-cover-img'); 51 | $imageURL = $this->image->getUrl(); 52 | 53 | $imageDimensions = $this->context->getMediaDimensions($imageURL, AMPContext::MEDIA_TYPE_IMAGE); 54 | $imageWidth = $imageDimensions[0]; 55 | $imageHeight = $imageDimensions[1]; 56 | 57 | $horizontalScale = AMPContext::DEFAULT_WIDTH / $imageWidth; 58 | $verticalScale = AMPContext::DEFAULT_HEIGHT / $imageHeight; 59 | $maxScale = max($horizontalScale, $verticalScale); 60 | 61 | $translateX = (int) (-($imageWidth * $maxScale - AMPContext::DEFAULT_WIDTH) / 2); 62 | $translateY = (int) (-($imageHeight * $maxScale - AMPContext::DEFAULT_HEIGHT) / 2); 63 | 64 | $imageWidth = (int) ($imageWidth * $maxScale); 65 | $imageHeight = (int) ($imageHeight * $maxScale); 66 | 67 | $this->ampImgTag->setAttribute('src', $imageURL); 68 | $this->ampImgTag->setAttribute('width', (string) $imageWidth); 69 | $this->ampImgTag->setAttribute('height', (string) $imageHeight); 70 | $this->ampImgTag->setAttribute('layout', 'responsive'); 71 | 72 | $imageCSSClass = $this->ampImgTag->getAttribute('class'); 73 | $containerCSSClass = $this->containerTag->getAttribute('class'); 74 | $this->context->getCssBuilder() 75 | ->addProperty("amp-img.$imageCSSClass", 'transform', "translate({$translateX}px, {$translateY}px)"); 76 | } 77 | 78 | public function build() 79 | { 80 | $this->genContainer(); 81 | $this->genAmpImage(); 82 | $this->genCaptionContainer(); 83 | 84 | $this->containerTag->appendChild($this->ampImgTag); 85 | return $this->containerTag; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/AMP/AMPHeader.php: -------------------------------------------------------------------------------- 1 | context = $context; 20 | } 21 | 22 | private function iaHeader() 23 | { 24 | return $this->context->getInstantArticle()->getHeader(); 25 | } 26 | 27 | private function genKicker() 28 | { 29 | if ($this->iaHeader()->getKicker()) { 30 | $kicker = $this->context->createElement('h2', $this->header, 'header-category'); 31 | $kicker->appendChild($this->context->getInstantArticle() 32 | ->getHeader() 33 | ->getKicker() 34 | ->textToDOMDocumentFragment($this->context->getDocument())); 35 | $this->context->buildSpacingDiv($this->header); 36 | } 37 | } 38 | 39 | private function genTitle() 40 | { 41 | $iaTitle = $this->iaHeader()->getTitle(); 42 | if ($iaTitle) { 43 | $h1 = $this->context->createElement('h1', $this->header, 'header-h1'); 44 | $h1->appendChild($iaTitle->textToDOMDocumentFragment($this->context->getDocument())); 45 | $this->context->buildSpacingDiv($this->header); 46 | } 47 | } 48 | 49 | private function genHeaderBar() 50 | { 51 | $this->headerBar = $this->context->createElement('div', $this->header, 'header-bar'); 52 | $this->context->buildSpacingDiv($this->header); 53 | // Note: The logo will be added after the whole article is processed 54 | } 55 | 56 | private function genSubtitle() 57 | { 58 | if ($this->iaHeader()->getSubtitle()) { 59 | $iaHeaderSubtitle = $this->iaHeader()->getSubtitle()->textToDOMDocumentFragment($this->context->getDocument()); 60 | $subtitle = $this->context->createElement('h2', $this->header, 'header-h2'); 61 | $subtitle->appendChild($iaHeaderSubtitle); 62 | 63 | $this->context->buildSpacingDiv($this->header); 64 | } 65 | } 66 | 67 | private function genArticlePublishDateElement() 68 | { 69 | $this->publishDateElement = $this->context->createElement('h3', $this->header, 'header-date'); 70 | // Note: The published date will be added after the whole article is processed 71 | $this->context->buildSpacingDiv($this->header); 72 | } 73 | 74 | private function genAuthors() 75 | { 76 | $authors = $this->context->createElement('h3', $this->header, 'header-author'); 77 | $authorsElement = $this->iaHeader()->getAuthors(); 78 | $authorsString = []; 79 | foreach ($authorsElement as $author) { 80 | $authorsString[] = $author->getName(); 81 | } 82 | $authors->appendChild($this->context->getDocument()->createTextNode('By '.implode($authorsString, ', '))); 83 | $this->context->buildSpacingDiv($this->header); 84 | } 85 | 86 | private function genContainer() 87 | { 88 | // Builds the content Header, with proper colors and image, adding to body 89 | $this->header = $this->context->createElement('header', $this->context->getBody(), 'header'); 90 | // Creates the cover content for the cover and appends to the header 91 | if ($this->context->getInstantArticle()->getHeader()->getCover()) { 92 | $ampCover = new AMPCover($this->context, $this->context->getInstantArticle()->getHeader()->getCover()); 93 | $this->header->appendChild($ampCover->build()); 94 | } 95 | } 96 | 97 | public function genHeaderLogo($logo) 98 | { 99 | if (!isset($logo->url)) { 100 | return; 101 | } 102 | 103 | $ampImageContainer = $this->context->createElement( 104 | 'div', 105 | $this->headerBar, 106 | 'header-bar-img-container' 107 | ); 108 | $ampImage = $this->context->createElement( 109 | 'amp-img', 110 | $ampImageContainer, 111 | null, 112 | array( 113 | 'src' => $logo->url, 114 | 'width' => $logo->width, 115 | 'height' => $logo->height 116 | ) 117 | ); 118 | } 119 | 120 | public function genArticlePublishDate($dateFormat) 121 | { 122 | $published = $this->iaHeader()->getPublished(); 123 | if ($published) { 124 | $datetime = $published->getDatetime(); 125 | $this->publishDateElement->appendChild( 126 | $this->context->getDocument()->createTextNode( 127 | date_format($datetime, $dateFormat) 128 | ) 129 | ); 130 | } 131 | } 132 | 133 | public function build() 134 | { 135 | $this->genContainer(); 136 | $this->genHeaderBar(); 137 | $this->genKicker(); 138 | $this->genTitle(); 139 | $this->genSubtitle(); 140 | $this->genAuthors(); 141 | $this->genArticlePublishDateElement(); 142 | 143 | return $this->header; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/AMP/AMPImage.php: -------------------------------------------------------------------------------- 1 | url = $url; 20 | $this->width = $width; 21 | $this->height = $height; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/AMP/configuration/global.amp.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. */ 2 | /* Global Styles */ 3 | .ia2amp-header-bar { 4 | margin: -5px 0 0 0; 5 | height: 55px; 6 | font: 0/0 a; 7 | } 8 | .ia2amp-header-bar:before { 9 | content: ' '; 10 | display: inline-block; 11 | vertical-align: middle; 12 | height: 100%; 13 | width: 16.4px; 14 | } 15 | .ia2amp-header-bar-img-container { 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | .ia2amp-header-category { 20 | font-size: 10px; 21 | line-height: 12px; 22 | display: block; 23 | font-weight: normal; 24 | } 25 | .ia2amp-header-h1 { 26 | font-size: 24px; 27 | line-height: 30px; 28 | display: block; 29 | font-weight: normal; 30 | } 31 | .ia2amp-header-h2 { 32 | font-size: 16px; 33 | line-height: 20px; 34 | display: block; 35 | font-weight: normal; 36 | } 37 | .ia2amp-header h3 { 38 | font-size: 8px; 39 | line-height: 12px; 40 | display: block; 41 | font-weight: normal; 42 | } 43 | 44 | .ia2amp-h1, .ia2amp-h2, .ia2amp-h3 { 45 | display: block; 46 | font-weight: normal; 47 | } 48 | 49 | .ia2amp-h1 { 50 | font-size: 19px; 51 | line-height: 23px; 52 | } 53 | 54 | .ia2amp-h2 { 55 | font-size: 16px; 56 | line-height: 20px; 57 | } 58 | 59 | .ia2amp-p { 60 | font-size: 14px; 61 | line-height: 20px; 62 | display: inline-block; 63 | 64 | 65 | } 66 | .ia2amp-p a { 67 | margin: 0; 68 | } 69 | 70 | .ia2amp-blockquote { 71 | font-size: 14px; 72 | line-height: 20px; 73 | padding: 0px 13.2px 0px 18.8px; 74 | } 75 | 76 | .ia2amp-spacing { 77 | display: block; 78 | height: 18.8px; 79 | margin: 0 16.4px 0 16.4px; 80 | } 81 | 82 | header figure figcaption { 83 | display: none; 84 | } 85 | 86 | .ia2amp-figure { 87 | margin: 0; 88 | } 89 | 90 | .ia2amp-op-small h1, .ia2amp-op-small h2 { 91 | font-size: 10px; 92 | display: block; 93 | } 94 | 95 | .ia2amp-op-medium h1, .ia2amp-op-medium h2 { 96 | font-size: 14px; 97 | display: block; 98 | } 99 | 100 | .ia2amp-op-large h1, .ia2amp-op-large h2 { 101 | font-size: 16px; 102 | display: block; 103 | } 104 | 105 | .ia2amp-op-extra-large h1, .ia2amp-op-extra-large h2 { 106 | font-size: 23px; 107 | display: block; 108 | } 109 | 110 | .ia2amp-figcaption cite { 111 | font-size: 8px; 112 | display: block; 113 | } 114 | 115 | .ia2amp-op-left { 116 | text-align: left; 117 | } 118 | 119 | .ia2amp-op-center { 120 | text-align: center; 121 | } 122 | 123 | .ia2amp-op-right { 124 | text-align: right; 125 | } 126 | 127 | .ia2amp-figure { 128 | position: relative; 129 | } 130 | 131 | figcaption.ia2amp-op-vertical-center { 132 | position: absolute; 133 | z-index: 1000; 134 | } 135 | 136 | .ia2amp-footer { 137 | display: block; 138 | font-size: 12px; 139 | line-height: 17px; 140 | } 141 | 142 | .ia2amp-footer aside p { 143 | margin: 0; 144 | } 145 | 146 | .ia2amp-spacing.after-header-bar.before-header-category { 147 | height: 18.8px; 148 | } 149 | 150 | .ia2amp-spacing.after-header-category.before-header-h1, 151 | .ia2amp-spacing.after-header-h1.before-header-h2 { 152 | height: 13.2px; 153 | } 154 | 155 | .ia2amp-spacing.after-h1.before-h2 { 156 | height: 13.2px; 157 | } 158 | 159 | .ia2amp-spacing.after-li.before-li { 160 | height: 13.2px; 161 | } 162 | 163 | .ia2amp-spacing.after-footer.before-footer { 164 | height: 18.8px; 165 | } 166 | 167 | .ia2amp-spacing.after-header-h1.before-header-author, 168 | .ia2amp-spacing.after-header-h2.before-header-author { 169 | height: 18.8px; 170 | } 171 | 172 | .ia2amp-spacing.after-header-author.before-header-date { 173 | height: 0; 174 | } 175 | 176 | .ia2amp-spacing.after-header-date.before-h1, 177 | .ia2amp-spacing.after-header-date.before-h2, 178 | .ia2amp-spacing.after-header-date.before-p { 179 | height: 26.4px; 180 | } 181 | 182 | .ia2amp-spacing.after-h1.before-p, 183 | .ia2amp-spacing.after-h2.before-p, 184 | .ia2amp-spacing.after-h3.before-p { 185 | height: 18.8px; 186 | } 187 | 188 | .ia2amp-spacing.after-blockquote.before-p { 189 | height: 18.8px; 190 | } 191 | 192 | .ia2amp-spacing.after-slideshow, 193 | .ia2amp-spacing.before-slideshow, 194 | .ia2amp-spacing.after-interactive, 195 | .ia2amp-spacing.before-interactive, 196 | .ia2amp-spacing.after-image, 197 | .ia2amp-spacing.before-image, 198 | .ia2amp-spacing.after-figcaption-small, 199 | .ia2amp-spacing.after-figcaption-medium, 200 | .ia2amp-spacing.after-figcaption-large, 201 | .ia2amp-spacing.after-figcaption-extra-large { 202 | height: 26.4px; 203 | } 204 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/Utils/CSSBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | * $cssBuilder = new CSSBuilder(); 16 | * $cssBuilder->addProperty('.someClass', 'width', '300px') 17 | * ->addProperty('.someClass', 'height', '400px') 18 | * ->addProperty('.otherClass', 'background-color', '#aabbcc') 19 | * ->addProperty('.otherClass', 'border-width', '2px'); 20 | * $result = $cssBuilder->build(true); 21 | * 22 | */ 23 | class CSSBuilder 24 | { 25 | /** 26 | * @var array('string'=>array('string' => properties)) 27 | */ 28 | private $selectors = array(); 29 | 30 | /** 31 | * @var string prefix for css selector classes 32 | */ 33 | private $prefix; 34 | 35 | /** 36 | * @var string spacing For height on the separator divs 37 | */ 38 | private $spacing; 39 | 40 | public function __construct($prefix = 'ia2amp-', $spacing = 'spacing') 41 | { 42 | $this->prefix = $prefix; 43 | $this->spacing = $spacing; 44 | } 45 | 46 | /** 47 | * Simple key => value method setting for CSS properties. This method does not apply any validation 48 | * be cautious about using it. 49 | * @param string $selector The selector for the CSS, free format accepted. Be cautious. 50 | * @param string $property The property name on CSS, free format accepted. Be cautious. 51 | * @param string $value The property value on CSS, free format accepted. Be cautious. 52 | * @return CSSBuilder $this instance. 53 | */ 54 | public function addProperty($selector, $property, $value) 55 | { 56 | if (isset($this->selectors[$selector])) { 57 | $selectorProps = $this->selectors[$selector]; 58 | } else { 59 | $selectorProps = array(); 60 | } 61 | 62 | $selectorProps[$property] = $value; 63 | $this->selectors[$selector] = $selectorProps; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Adds property and value to the $class. It creates the selector, apply prefix and builds up 70 | * css selector class grouping the properties. 71 | * @param string|array $class The class to be applied to the selector. 72 | * @param string $property The property name on CSS, free format accepted. Be cautious. 73 | * @param string $value The property value on CSS, free format accepted. Be cautious. 74 | * @return CSSBuilder $this instance. 75 | */ 76 | public function addToSelector($class, $property, $value) 77 | { 78 | return $this->addProperty($this->buildCssSelector($class), $property, $value); 79 | } 80 | 81 | /** 82 | * Adds dimension sized property value to the $class. It creates the selector, apply prefix and builds up 83 | * css selector class grouping the properties. If value not informed, zero will be placed. 84 | * @param string|array $class The class to be applied to the selector. 85 | * @param string $property The property name on CSS, free format accepted. Be cautious. 86 | * @param string|float|int $dimension The property value on CSS. 87 | * @param string $unit The unit to be applied. Default value: 'px'. 88 | * @return CSSBuilder $this instance. 89 | */ 90 | public function addDimensionToSelector($class, $property, $dimension, $unit = 'px') 91 | { 92 | return $this->addProperty($this->buildCssSelector($class), $property, $dimension ? $dimension.$unit : '0'); 93 | } 94 | 95 | /** 96 | * Adds condensed dimensions to property value selected by $class. If value not informed, zero will be placed. 97 | * @param string|array $class The class to be applied to the selector. 98 | * @param string $property The property name on CSS, free format accepted. Be cautious. 99 | * @param string|float|int $top The property value for top position on CSS. 100 | * @param string|float|int $right The property value for right position on CSS. 101 | * @param string|float|int $bottom The property value for bottom position on CSS. 102 | * @param string|float|int $left The property value for left position on CSS. 103 | * * @param string $unit The unit to be applied. Default value: 'px'. 104 | * @return CSSBuilder $this instance. 105 | */ 106 | public function addTopRightBottomLeftToSelector($class, $property, $top, $right, $bottom, $left, $unit = 'px') 107 | { 108 | $dimension = 109 | ($top ? $top.$unit : '0').' '. 110 | ($right ? $right.$unit : '0').' '. 111 | ($bottom ? $bottom.$unit : '0').' '. 112 | ($left ? $left.$unit : '0'); 113 | return $this->addProperty($this->buildCssSelector($class), $property, $dimension); 114 | } 115 | 116 | /** 117 | * Adds spacing height value to the proper selected class. 118 | * @param string|array $class The class to be applied to the selector. 119 | * @param string|float|int $height The spacing value for spacing. 120 | * @param string $unit The unity to be applied. Default value: 'px'. 121 | * @return CSSBuilder $this instance. 122 | */ 123 | public function addHeightSpacingToSelector($class, $height, $unit = 'px') 124 | { 125 | return $this->addProperty( 126 | $this->buildCssSelector($class).' + '.$this->buildCssSelector($this->spacing), 127 | 'height', 128 | $height ? $height.$unit : '0' 129 | ); 130 | } 131 | 132 | 133 | private function buildCssClass($cssClassName) 134 | { 135 | return $this->prefix.$cssClassName; 136 | } 137 | 138 | private function buildCssSelector($class) 139 | { 140 | if (is_array($class)) { 141 | $selectors = array(); 142 | foreach ($class as $singleClass) { 143 | $selectors[] = $this->buildCssSelector($singleClass); 144 | } 145 | return implode(', ', $selectors); 146 | } else { 147 | return '.'.$this->buildCssClass($class); 148 | } 149 | } 150 | 151 | 152 | /** 153 | * Builds the css output representing the current status of the CSSBuilder structure. 154 | * @param $formatOutput boolean Indicates if output will be formated. 155 | * @return string The CSS generated based on current status. 156 | */ 157 | public function build($formatOutput = true) 158 | { 159 | $result = ''; 160 | $formatIdent = $formatOutput ? ' ' : ''; 161 | $formatNewLine = $formatOutput ? "\n" : ''; 162 | foreach ($this->selectors as $selector => $properties) { 163 | if ($properties && !empty($properties)) { 164 | if ($result !== '') { 165 | $result = $result.$formatNewLine.$formatNewLine; 166 | } 167 | 168 | $result = $result.$selector.' {'.$formatNewLine; 169 | foreach ($properties as $property => $value) { 170 | $result = $result.$formatIdent.$property.': '.$value.';'; 171 | $result = $result.$formatNewLine; 172 | } 173 | $result = $result.'}'; 174 | } 175 | } 176 | 177 | return $result; 178 | } 179 | 180 | /** 181 | * Auxiliary method to extract the full qualified class name. 182 | * 183 | * @return string The full qualified name of class. 184 | */ 185 | public static function getClassName() 186 | { 187 | return get_called_class(); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/Utils/CallbackHook.php: -------------------------------------------------------------------------------- 1 | methods 19 | */ 20 | public $callbacks = array(); 21 | 22 | /** 23 | * @var array The priority keys of actively running iterations of a hook. 24 | */ 25 | private $iterations = array(); 26 | 27 | /** 28 | * @var array The current priority of actively running iterations of a hook. 29 | */ 30 | private $currentPriority = array(); 31 | 32 | /** 33 | * @var int Number of levels this hook can be recursively called. 34 | */ 35 | private $nestingLevel = 0; 36 | 37 | /** 38 | * Hooks a function or method to a specific filter action. 39 | * 40 | * @param string $tag The name of the filter to hook the $functionToAdd callback to. 41 | * @param callable $functionToAdd The callback to be run when the filter is applied. 42 | * @param int $priority The order in which the functions associated with a 43 | * particular action are executed. Lower numbers correspond with 44 | * earlier execution, and functions with the same priority are executed 45 | * in the order in which they were added to the action. 46 | * @param int $acceptedArgs The number of arguments the function accepts. 47 | */ 48 | public function addFilter($idx, $tag, $functionToAdd, $priority, $acceptedArgs) 49 | { 50 | $priorityExisted = isset($this->callbacks[$priority]); 51 | 52 | $this->callbacks[$priority][$idx] = array( 53 | 'function' => $functionToAdd, 54 | 'accepted_args' => $acceptedArgs 55 | ); 56 | 57 | // if we're adding a new priority to the list, put them back in sorted order 58 | if (!$priorityExisted && count($this->callbacks) > 1) { 59 | ksort($this->callbacks, SORT_NUMERIC); 60 | } 61 | 62 | if ($this->nestingLevel > 0) { 63 | $this->resortPerPriority($priority, $priorityExisted); 64 | } 65 | } 66 | 67 | /** 68 | * Handles reseting callback priority keys mid-iteration. 69 | * 70 | * @param bool|int $newPriority Optional. The priority of the new filter being added. Default false, 71 | * for no priority being added. 72 | * @param bool $priorityExisted Optional. Flag for whether the priority already existed before the new 73 | * filter was added. Default false. 74 | */ 75 | private function resortPerPriority($newPriority = false, $priorityExisted = false) 76 | { 77 | $newPriorities = array_keys($this->callbacks); 78 | 79 | // If there are no remaining hooks, clear out all running iterations. 80 | if (! $newPriorities) { 81 | foreach ($this->iterations as $index => $iteration) { 82 | $this->iterations[$index] = $newPriorities; 83 | } 84 | return; 85 | } 86 | 87 | $min = min($newPriorities); 88 | foreach ($this->iterations as $index => &$iteration) { 89 | $current = current($iteration); 90 | // If we're already at the end of this iteration, just leave the array pointer where it is. 91 | if (false === $current) { 92 | continue; 93 | } 94 | 95 | $iteration = $newPriorities; 96 | 97 | if ($current < $min) { 98 | array_unshift($iteration, $current); 99 | continue; 100 | } 101 | 102 | while (current($iteration) < $current) { 103 | if (false === next($iteration)) { 104 | break; 105 | } 106 | } 107 | 108 | // If we have a new priority that didn't exist, but ::applyFilters() thinks it's the current priority... 109 | if ($newPriority === $this->currentPriority[$index] && ! $priorityExisted) { 110 | /* 111 | * ... and the new priority is the same as what $this->iterations thinks is the previous 112 | * priority, we need to move back to it. 113 | */ 114 | 115 | if (false === current($iteration)) { 116 | // If we've already moved off the end of the array, go back to the last element. 117 | $prev = end($iteration); 118 | } else { 119 | // Otherwise, just go back to the previous element. 120 | $prev = prev($iteration); 121 | } 122 | if (false === $prev) { 123 | // Start of the array. Reset, and go about our day. 124 | reset($iteration); 125 | } elseif ($newPriority !== $prev) { 126 | // Previous wasn't the same. Move forward again. 127 | next($iteration); 128 | } 129 | } 130 | } 131 | unset($iteration); 132 | } 133 | 134 | /** 135 | * Unhooks a function or method from a specific filter action. 136 | * 137 | * @param string $tag The filter hook to which the function to be removed is hooked. Used 138 | * for building the callback ID when SPL is not available. 139 | * @param int $priority The exact priority used when adding the original filter callback. 140 | * @return bool Whether the callback existed before it was removed. 141 | */ 142 | public function removeFilter($idx, $tag, $priority) 143 | { 144 | $exists = isset($this->callbacks[$priority][$idx]); 145 | if ($exists) { 146 | unset($this->callbacks[$priority][$idx]); 147 | if (! $this->callbacks[$priority]) { 148 | unset($this->callbacks[$priority]); 149 | if ($this->nestingLevel > 0) { 150 | $this->resortPerPriority(); 151 | } 152 | } 153 | } 154 | return $exists; 155 | } 156 | 157 | /** 158 | * Checks if a specific action has been registered for this hook. 159 | * 160 | * @param callable|bool $functionToCheck Optional. The callback to check for. Default false. 161 | * @param string $tag Optional. The name of the filter hook. Used for building 162 | * the callback ID when SPL is not available. Default empty. 163 | * @return bool|int The priority of that hook is returned, or false if the function is not attached. 164 | */ 165 | public function hasFilter($idx, $tag = '', $functionToCheck = false) 166 | { 167 | if (false === $functionToCheck) { 168 | return $this->hasFilters(); 169 | } 170 | 171 | if (! $idx) { 172 | return false; 173 | } 174 | 175 | foreach ($this->callbacks as $priority => $callbacks) { 176 | if (isset($callbacks[$idx])) { 177 | return $priority; 178 | } 179 | } 180 | 181 | return false; 182 | } 183 | 184 | /** 185 | * Checks if any callbacks have been registered for this hook. 186 | * 187 | * @return bool True if callbacks have been registered for the current hook, otherwise false. 188 | */ 189 | public function hasFilters() 190 | { 191 | foreach ($this->callbacks as $callbacks) { 192 | if ($callbacks) { 193 | return true; 194 | } 195 | } 196 | return false; 197 | } 198 | 199 | /** 200 | * Removes all callbacks from the current filter. 201 | * 202 | * @param int|bool $priority Optional. The priority number to remove. Default false. 203 | */ 204 | public function removeAllFilters($priority = false) 205 | { 206 | if (! $this->callbacks) { 207 | return; 208 | } 209 | 210 | if (false === $priority) { 211 | $this->callbacks = array(); 212 | } else if (isset($this->callbacks[$priority])) { 213 | unset($this->callbacks[$priority]); 214 | } 215 | 216 | if ($this->nestingLevel > 0) { 217 | $this->resortPerPriority(); 218 | } 219 | } 220 | 221 | /** 222 | * Calls the callback functions added to a filter hook. 223 | * 224 | * @param mixed $value The value to filter. 225 | * @param array $args Arguments to pass to callbacks. 226 | * @return mixed The filtered value after all hooked functions are applied to it. 227 | */ 228 | public function applyFilters($value, $args) 229 | { 230 | if (!$this->callbacks) { 231 | return $value; 232 | } 233 | 234 | $nestingLevel = $this->nestingLevel++; 235 | 236 | $this->iterations[$nestingLevel] = array_keys($this->callbacks); 237 | $num_args = count($args); 238 | 239 | do { 240 | $this->currentPriority[$nestingLevel] = $priority = current($this->iterations[$nestingLevel]); 241 | 242 | foreach ($this->callbacks[$priority] as $callbackPriority) { 243 | $args[0] = $value; 244 | 245 | // Avoid the array_slice if possible. 246 | if ($callbackPriority['accepted_args'] == 0) { 247 | $value = call_user_func_array($callbackPriority['function'], array()); 248 | } elseif ($callbackPriority['accepted_args'] >= $num_args) { 249 | $value = call_user_func_array($callbackPriority['function'], $args); 250 | } else { 251 | $value = call_user_func_array($callbackPriority['function'], array_slice($args, 0, (int)$callbackPriority['accepted_args'])); 252 | } 253 | } 254 | } while (false !== next($this->iterations[$nestingLevel])); 255 | 256 | unset($this->iterations[$nestingLevel]); 257 | unset($this->currentPriority[$nestingLevel]); 258 | 259 | $this->nestingLevel--; 260 | 261 | return $value; 262 | } 263 | 264 | /** 265 | * Return the current priority level of the currently running iteration of the hook. 266 | * 267 | * @return int|false If the hook is running, return the current priority level. If it isn't running, return false. 268 | */ 269 | public function currentPriority() 270 | { 271 | if (false === current($this->iterations)) { 272 | return false; 273 | } 274 | 275 | return current(current($this->iterations)); 276 | } 277 | 278 | /** 279 | * Determines whether an offset value exists. 280 | * 281 | * @link http://php.net/manual/en/arrayaccess.offsetexists.php 282 | * 283 | * @param mixed $offset An offset to check for. 284 | * @return bool True if the offset exists, false otherwise. 285 | */ 286 | public function offsetExists($offset) 287 | { 288 | return isset($this->callbacks[$offset]); 289 | } 290 | 291 | /** 292 | * Retrieves a value at a specified offset. 293 | * 294 | * @link http://php.net/manual/en/arrayaccess.offsetget.php 295 | * 296 | * @param mixed $offset The offset to retrieve. 297 | * @return mixed If set, the value at the specified offset, null otherwise. 298 | */ 299 | public function offsetGet($offset) 300 | { 301 | return isset($this->callbacks[$offset]) ? $this->callbacks[$offset] : null; 302 | } 303 | 304 | /** 305 | * Sets a value at a specified offset. 306 | * 307 | * @link http://php.net/manual/en/arrayaccess.offsetset.php 308 | * 309 | * @param mixed $offset The offset to assign the value to. 310 | * @param mixed $value The value to set. 311 | */ 312 | public function offsetSet($offset, $value) 313 | { 314 | if (is_null($offset)) { 315 | $this->callbacks[] = $value; 316 | } else { 317 | $this->callbacks[$offset] = $value; 318 | } 319 | } 320 | 321 | /** 322 | * Unsets a specified offset. 323 | * 324 | * @link http://php.net/manual/en/arrayaccess.offsetunset.php 325 | * 326 | * @param mixed $offset The offset to unset. 327 | */ 328 | public function offsetUnset($offset) 329 | { 330 | unset($this->callbacks[$offset]); 331 | } 332 | 333 | /** 334 | * Returns the current element. 335 | * 336 | * @link http://php.net/manual/en/iterator.current.php 337 | * 338 | * @return array Of callbacks at current priority. 339 | */ 340 | public function current() 341 | { 342 | return current($this->callbacks); 343 | } 344 | 345 | /** 346 | * Moves forward to the next element. 347 | * 348 | * @link http://php.net/manual/en/iterator.next.php 349 | * 350 | * @return array Of callbacks at next priority. 351 | */ 352 | public function next() 353 | { 354 | return next($this->callbacks); 355 | } 356 | 357 | /** 358 | * Returns the key of the current element. 359 | * 360 | * @link http://php.net/manual/en/iterator.key.php 361 | * 362 | * @return mixed Returns current priority on success, or NULL on failure 363 | */ 364 | public function key() 365 | { 366 | return key($this->callbacks); 367 | } 368 | 369 | /** 370 | * Checks if current position is valid. 371 | * 372 | * @link http://php.net/manual/en/iterator.valid.php 373 | * 374 | * @return boolean 375 | */ 376 | public function valid() 377 | { 378 | return key($this->callbacks) !== null; 379 | } 380 | 381 | /** 382 | * Rewinds the Iterator to the first element. 383 | * 384 | * @link http://php.net/manual/en/iterator.rewind.php 385 | */ 386 | public function rewind() 387 | { 388 | reset($this->callbacks); 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/Utils/Hook.php: -------------------------------------------------------------------------------- 1 | 15 | * // Obtain the hook instance or create one 16 | * Hook::create(); 17 | * // Name your hook with the string you want and set the default function to be called. 18 | * $result = $hook->call('hook_name', array('Facebook\InstantArticles\hook\HookTest', 'staticMethodHookParams'), array('param1', 'param2')); 19 | * // Anyone can override your callable by using this line: 20 | * $hook->setHook('hook_name', array($this, 'methodHookReplacing')); 21 | * 22 | */ 23 | class Hook 24 | { 25 | /* Hooks to have overriden callable */ 26 | private $hooks = array(); 27 | private $params = array(); 28 | 29 | /* Hooks to have before callable */ 30 | private $beforeHooks = array(); 31 | private $beforeParams = array(); 32 | 33 | /* Hooks to have after callable */ 34 | private $afterHooks = array(); 35 | private $afterParams = array(); 36 | 37 | /** 38 | * Private constructor to force factory method: Hook::create(); 39 | */ 40 | private function __construct() 41 | { 42 | } 43 | 44 | /** 45 | * @return Hook new instance of the Hook class. 46 | */ 47 | public static function create() 48 | { 49 | return new self(); 50 | } 51 | 52 | public function clearHooks($hookName) 53 | { 54 | $this->removeHook($hookName); 55 | $this->removeAfterHook($hookName); 56 | $this->removeBeforeHook($hookName); 57 | } 58 | 59 | /** 60 | * Overrides a hook by name, by setting a new callable, with params. 61 | * @param string $hookName The name for callable instruction to be intercepted/hooked. Use different names if you need different hooks. 62 | * @param string/array $callable The string method to be called or array with array('Namespace.ClassName', 'methodName') 63 | * @param array(mixed) The params to be used into your methodName from your callable. 64 | */ 65 | public function setHook($hookName, $callable, $params = null) 66 | { 67 | $this->hooks[$hookName] = $callable; 68 | if (isset($params) && $params) { 69 | $this->params[$hookName] = $params; 70 | } 71 | } 72 | 73 | public function removeHook($hookName) 74 | { 75 | unset($this->hooks[$hookName]); 76 | } 77 | 78 | /** 79 | * Overrides a before event hook by name, by setting a new callable, with params. The returned value from the beforeHook will be ignored. There is no blocking system. 80 | * @param string $hookName The name for callable instruction to be intercepted/hooked. Use different names if you need different hooks. 81 | * @param string/array $callable The string method to be called or array with array('Namespace.ClassName', 'methodName') 82 | * @param array(mixed) The params to be used into your methodName from your callable. 83 | */ 84 | public function setBeforeHook($hookName, $callable, $params = null) 85 | { 86 | $this->beforeHooks[$hookName] = $callable; 87 | if (isset($params) && $params) { 88 | $this->beforeParams[$hookName] = $params; 89 | } 90 | } 91 | 92 | public function removeBeforeHook($hookName) 93 | { 94 | unset($this->beforeHooks[$hookName]); 95 | } 96 | 97 | /** 98 | * Overrides a after event hook by name, by setting a new callable, with params. The returned value from the after will be ignored. 99 | * @param string $hookName The name for callable instruction to be intercepted/hooked. Use different names if you need different hooks. 100 | * @param string/array $callable The string method to be called or array with array('Namespace.ClassName', 'methodName') 101 | * @param array(mixed) The params to be used into your methodName from your callable. 102 | */ 103 | public function setAfterHook($hookName, $callable, $params = null) 104 | { 105 | $this->afterHooks[$hookName] = $callable; 106 | if (isset($params) && $params) { 107 | $this->afterParams[$hookName] = $params; 108 | } 109 | } 110 | 111 | public function removeAfterHook($hookName) 112 | { 113 | unset($this->afterHooks[$hookName]); 114 | } 115 | 116 | /** 117 | * Any callable method/call that you want to make it possible to have it 118 | * possible to be overriden or a before/after hook, just call it by name 119 | * using your hook instance. This make the callable to be intercepted/overriden. 120 | * @param string $hookName The name for callable instruction to be intercepted/hooked. Use different names if you need different hooks. 121 | * @param string/array $callable The string method to be called or array with array('Namespace.ClassName', 'methodName') 122 | * @param array(mixed) The params to be used into your methodName from your callable. 123 | */ 124 | public function call($hookName, $callable, $params = array()) 125 | { 126 | // Treats before hook is called. To set something to happen before any hook, just use setBeforeHook('hook_name', ...) 127 | if (array_key_exists($hookName, $this->beforeHooks) && isset($this->beforeHooks[$hookName])) { 128 | $beforeCallable = $this->beforeHooks[$hookName]; 129 | $beforeParams = array(); 130 | if (array_key_exists($hookName, $this->beforeParams) && isset($this->beforeParams[$hookName])) { 131 | $beforeParams = $this->beforeParams[$hookName]; 132 | } 133 | // Calls the "before" event $hookName, ignoring the return 134 | call_user_func_array($beforeCallable, $beforeParams); 135 | } 136 | 137 | // Treats the Hook itself 138 | if (array_key_exists($hookName, $this->hooks) && isset($this->hooks[$hookName])) { 139 | $callable = $this->hooks[$hookName]; 140 | if (array_key_exists($hookName, $this->params) && isset($this->params[$hookName])) { 141 | $params = $this->params[$hookName]; 142 | } 143 | } 144 | $return = call_user_func_array($callable, $params); 145 | 146 | 147 | // Treats after hook is called. To set something to happen after any hook, just use setAfterHook('hook_name', ...) 148 | if (array_key_exists($hookName, $this->afterHooks) && isset($this->afterHooks[$hookName])) { 149 | $afterCallable = $this->afterHooks[$hookName]; 150 | $afterParams = array(); 151 | if (array_key_exists($hookName, $this->afterParams) && isset($this->afterParams[$hookName])) { 152 | $afterParams = $this->afterParams[$hookName]; 153 | } 154 | 155 | // Calls the "after" event $hookName ignoring the return 156 | call_user_func_array($afterCallable, $afterParams); 157 | } 158 | 159 | return $return; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/Utils/Observer.php: -------------------------------------------------------------------------------- 1 | 17 | * // Obtain the observer instance or create one 18 | * $observer = Observer::create(); 19 | * // Name your hook with the string you want and set the default function to be called. 20 | * $result = $obs->applyFilters('filter name', SomeClass::statciMethodBeingCalled('param1', 'param2')); 21 | * // Anyone can override your callable by using this line: 22 | * $obs->addFilter('filter name', array($this, 'methodHookReplacing')); 23 | * 24 | */ 25 | class Observer 26 | { 27 | private static $filterCount = 0; 28 | 29 | /* Filters configured. Mapped array: 'filter name' => CallbackHolder */ 30 | private $callbacks = array(); 31 | 32 | /** 33 | * Private constructor to force factory method: Hook::create(); 34 | */ 35 | private function __construct() 36 | { 37 | } 38 | 39 | /** 40 | * @return Hook new instance of the Hook class. 41 | */ 42 | public static function create() 43 | { 44 | return new self(); 45 | } 46 | 47 | /** 48 | * Hook functions/methods to change, replace or add info to the data you are filtering. 49 | * 50 | * Anyone can modify data by binding a callback to a filter hook. When the filter 51 | * is later applied, each bound callback is run in order of priority, and given 52 | * the opportunity to modify a value by returning a new value or modifying the 53 | * value orinally returned. 54 | * 55 | * Check the following example on how we can bind callback to a filter: 56 | * 57 | * function myCallback( $value ) { 58 | * // Maybe modify $value in some way. 59 | * return $value; 60 | * } 61 | * $observer = Observer::create(); 62 | * $observer->addFilter('myFilterName', 'myCallback'); 63 | * 64 | * Bound callbacks can receive zero to the total defined in addFilter method call. 65 | * 66 | * Here are few more examples about how to call the apply filters and how to set it up 67 | * on the addFilter method. For example: 68 | * 69 | * // Call to be filtered 70 | * $value = $observer->applyFilters('filterName', $value, $arg2, $arg3); 71 | * 72 | * // Accepting zero/one arguments. 73 | * function callbackFunction() { 74 | * ... 75 | * return 'value overriden'; 76 | * } 77 | * $observer = Observer::create(); 78 | * $observer->addFilter('filterName', 'callbackFunction'); // Where $priority is default 10, $acceptedArgs is default 1. 79 | * 80 | * // Accepting two arguments (three possible). 81 | * function callbackFunction( $value, $arg2 ) { 82 | * ... 83 | * return $modifiedValue; 84 | * } 85 | * $observer = Observer::create(); 86 | * $observer->addFilter( 'filterName', 'callbackFunction', 5, 2 ); // Where $priority is 5, $acceptedArgs is 2. 87 | * 88 | * @param string $tag The name of the filter hook. 89 | * @param callable $functionToAdd The callback to be called when the filter is applied. 90 | * @param int $priority Optional. Priority order to run the multiple hooks appended to same $tag. 91 | * As lower the number is, more priority it will have. Default 10. 92 | * @param int $acceptedArgs Optional. The number of arguments the function accepts. Default 1. 93 | */ 94 | public function addFilter($tag, $functionToAdd, $priority = 10, $acceptedArgs = 1) 95 | { 96 | // This is a crude filter, needs to create the CallbackHook manager for that 97 | if (!isset($this->callbacks[$tag])) { 98 | $this->callbacks[$tag] = new CallbackHook(); 99 | } 100 | // Builds unique identifier for this callback call. This is important due to 101 | // same method calls from different instances. 102 | $idx = $this->getUniqueIndexID($tag, $functionToAdd); 103 | $this->callbacks[$tag]->addFilter($idx, $tag, $functionToAdd, $priority, $acceptedArgs); 104 | } 105 | 106 | /** 107 | * Call all the functions hooked to that filter name. If none are hooked the value will be returned back. 108 | * 109 | * A good strategy to use this function is to create hooking points into your code, by easily calling the 110 | * applyFilters() method. 111 | * 112 | * Check this example: 113 | * $observer = Observer::create(); 114 | * $url = $observer->applyFilters('GET_THE_URL', $a_url); 115 | * 116 | * You might not have initially a filter designed to this URL getter, but in 117 | * the future you might have a URL redirect or even https enforcement, or anything 118 | * 119 | * When callback functions get attached to this 'GET_THE_URL' hook name, it will 120 | * filter and can modify the final result. 121 | * 122 | * Full example: 123 | * 124 | * // The filter callback function 125 | * function myCallback($value, $param1, $param2) { 126 | * // do something with the value 127 | * return $value; 128 | * } 129 | * $observer = Observer::create(); 130 | * $observer->addFilter('filter name', 'myCallback', 10, 3); 131 | * 132 | * /* 133 | * * Apply the filters calling the 'myCallback' function we 134 | * * "hooked" to $tag using the addFilter() method. 135 | * * 'filter name' is the filter hook $tag 136 | * * 'value to filter' is the value being filtered 137 | * * $param1 and $param2 are the additional arguments passed to the callback. 138 | * $value = $observer->applyFilters('filter name', 'value to filter', $param1, $param2); 139 | * 140 | * @param string $tag The name of the filter hook. 141 | * @param mixed $value The value on which the filters hooked to `$tag` are applied on. 142 | * @param mixed $var,... Additional variables passed to the functions hooked to `$tag`. 143 | * @return mixed The filtered value after all hooked functions are applied to it. 144 | */ 145 | public function applyFilters($tag, $value/*, $var...*/) 146 | { 147 | $args = array(); 148 | 149 | // In case no filters configured for this hook, simply return informed value. 150 | if (!isset($this->callbacks[$tag])) { 151 | return $value; 152 | } 153 | 154 | // Uses this to be compatible with PHP < 5.6 155 | $args = func_get_args(); 156 | 157 | // Removes the $tag, since this is not an expected parameter to callbacks. 158 | array_shift($args); 159 | 160 | $filtered = $this->callbacks[$tag]->applyFilters($value, $args); 161 | 162 | return $filtered; 163 | } 164 | 165 | /** 166 | * Removes a function from a specified filter hook. 167 | * 168 | * This function removes a specific function attached to a filter hook. 169 | * To remove a hook, the $functionToRemove and $priority arguments must match 170 | * when the hook was added by calling #addFilter() method. 171 | * 172 | * @param string $tag The filter hook name where function + priority will be removed from 173 | * @param callable $functionToRemove The function/instance->method will be removed. 174 | * @param int $priority Optional. The priority of the function to be removed. Default 10. 175 | * @return bool True if found function to remove from filter list, false otherwise. 176 | */ 177 | public function removeFilter($tag, $functionToRemove, $priority = 10) 178 | { 179 | $return = false; 180 | if (isset($this->callbacks[$tag])) { 181 | $idx = $this->getUniqueIndexID($tag, $functionToRemove); 182 | $return = $this->callbacks[$tag]->removeFilter($idx, $tag, $priority); 183 | if (! $this->callbacks[$tag]->callbacks) { 184 | unset($this->callbacks[$tag]); 185 | } 186 | } 187 | return $return; 188 | } 189 | 190 | /** 191 | * Remove all of the hooks from a filter. 192 | * 193 | * @param string $tag The filter to remove hooks from. 194 | * @param int|bool $priority Optional. The priority number to remove. Default false. 195 | */ 196 | public function removeAllFilters($tag, $priority = false) 197 | { 198 | if (isset($this->callbacks[$tag])) { 199 | $this->callbacks[$tag]->removeAllFilters($priority); 200 | if (!$this->callbacks[$tag]->hasFilters()) { 201 | unset($this->callbacks[$tag]); 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Serialize and generates an index for storage and retrival of method/functions as callbacks. 208 | * 209 | * This index will be used mostly for instance method callback hooks. Since those cam imply into 210 | * having conflicting names. 211 | * 212 | * 213 | * $bar1 = new Bar(); 214 | * $bar2 = new Bar(); 215 | * 216 | * // Check that the callbacks are registered under same method name, same class, 217 | * // same priority, but using different instances. 218 | * $observer->addFilter('FILTER_NAME', array($bar1, 'foo'), 10); 219 | * $observer->addFilter('FILTER_NAME', array($bar2, 'foo'), 10); 220 | * 221 | * 222 | * @param string $tag Used in counting how many hooks were applied 223 | * @param callable $function Used for creating unique id 224 | * @param int|bool $priority Used in counting how many hooks were applied. If === false 225 | * and $function is an object reference, we return the unique id only if it already has one, 226 | * false otherwise. 227 | * @return string|false Unique ID for usage as array key or false if $priority === false 228 | * and $function is an object reference, and it does not already have a unique id. 229 | */ 230 | private function getUniqueIndexID($tag, $function) 231 | { 232 | if (is_string($function)) { 233 | return $function; 234 | } 235 | if ($function instanceof \Closure) { 236 | // If a Closure is used, it cannot be retrieved or removed 237 | // individually later 238 | return 'Closure' . self::$filterCount++; 239 | } 240 | if (is_object($function)) { 241 | $function = array($function, ''); 242 | } else { 243 | $function = (array) $function; 244 | } 245 | 246 | if (is_string($function[0])) { 247 | // Static call 248 | return $function[0] . '::' . $function[1]; 249 | } elseif (is_object($function[0])) { 250 | // Instance call 251 | if (function_exists('spl_object_hash')) { 252 | return spl_object_hash($function[0]).'->' . $function[1]; 253 | } else { 254 | $obj_idx = get_class($function[0]).'->'.$function[1]; 255 | if (!isset($function[0]->filterIdx)) { 256 | $obj_idx = $obj_idx . self::$filterCount; 257 | $function[0]->filterIdx = self::$filterCount; 258 | self::$filterCount++; 259 | } else { 260 | $obj_idx = $obj_idx . $function[0]->filterIdx; 261 | } 262 | return $obj_idx; 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * Check if any filter has been registered for a hook. 269 | * 270 | * @param string $tag The name of the filter. 271 | * @param callable|bool $functionToCheck Optional. Callable that will be checked. 272 | * @return false|int If $functionToCheck is omitted, returns boolean for whether the hook has 273 | * anything registered. With a specific function, the priority of that 274 | * hook is returned, false otherwise. 275 | */ 276 | public function hasFilter($tag, $functionToCheck = false) 277 | { 278 | // If hook name has nothing, then nothing is hooked there. 279 | if (!isset($this->callbacks[$tag])) { 280 | return false; 281 | } 282 | $idx = $this->getUniqueIndexID($tag, $functionToCheck); 283 | return $this->callbacks[$tag]->hasFilter($idx, $tag, $functionToCheck); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/Facebook/InstantArticles/Utils/Warning.php: -------------------------------------------------------------------------------- 1 | message = $message; 22 | $this->context = $context; 23 | $this->exception = $exception; 24 | } 25 | 26 | public function __toString() 27 | { 28 | $finalMessage = $this->message; 29 | 30 | if ($this->context !== null) { 31 | $finalMessage = $finalMessage."\nObject in the context: ".Type::stringify($this->context); 32 | } 33 | 34 | if ($this->exception) { 35 | $finalMessage = $finalMessage."\nException cause: ".Type::stringify($this->exception); 36 | } 37 | 38 | return $finalMessage; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/AMPCaptionTest.php: -------------------------------------------------------------------------------- 1 | withHeader( 25 | Header::create() 26 | ->withTitle('Big Top Title') 27 | ->withSubTitle('Smaller SubTitle') 28 | ->withPublishTime( 29 | Time::create(Time::PUBLISHED) 30 | ->withDatetime( 31 | \DateTime::createFromFormat( 32 | 'j-M-Y G:i:s', 33 | '14-Aug-1984 19:30:00' 34 | ) 35 | ) 36 | ) 37 | ->addAuthor( 38 | Author::create() 39 | ->withName('Author One') 40 | ->withDescription('Passionate coder and mountain biker') 41 | ) 42 | ->addAuthor( 43 | Author::create() 44 | ->withName('Author Two') 45 | ->withDescription('Weend surfer with heavy weight coding skils') 46 | ->withURL('http://facebook.com/author') 47 | ) 48 | ->withKicker('Some kicker of this article') 49 | ->withCover( 50 | Image::create() 51 | ->withURL('http://blog.wod.expert/wp-content/uploads/2017/03/fail1.jpg') 52 | ->withCaption( 53 | Caption::create() 54 | ->appendText('Some caption to the image') 55 | ) 56 | ) 57 | ); 58 | } 59 | 60 | private function genContext($document, $instantArticle) 61 | { 62 | $context = AMPContext::create($document, $instantArticle); 63 | 64 | $mediaSizes = array(); 65 | $mediaCacheFolder = __DIR__ . '/articles/media-cache'; 66 | $enableDownloadForMediaSizing = false; 67 | $defaultWidth = 1000; 68 | $defaultHeight = 900; 69 | 70 | $context->withMediaSizingSetup($mediaSizes, $mediaCacheFolder, $enableDownloadForMediaSizing, $defaultWidth, $defaultHeight); 71 | 72 | return $context; 73 | } 74 | 75 | public function testCaptionOnElementBottomByDefault() 76 | { 77 | $expected = 78 | '
'. 79 | ''. 80 | '
Some caption to the image
'. 81 | '
'; 82 | $instantArticle = $this->genInstantArticle(); 83 | 84 | $document = new \DOMDocument(); 85 | $context = $this->genContext($document, $instantArticle); 86 | 87 | $ampImg = $document->createElement('amp-img'); 88 | $result = AMPCaption::create($instantArticle->getHeader()->getCover()->getCaption(), $context, $ampImg)->build(); 89 | $this->assertEquals($expected, $result->ownerDocument->saveXML($result)); 90 | } 91 | 92 | public function testCaptionOnTopOfElement() 93 | { 94 | $expected = 95 | '
'. 96 | '
Some caption to the image
'. 97 | ''. 98 | '
'; 99 | $instantArticle = $this->genInstantArticle(); 100 | $instantArticle->getHeader()->getCover()->getCaption()->withPosition(Caption::POSITION_ABOVE); 101 | 102 | $document = new \DOMDocument(); 103 | $context = $this->genContext($document, $instantArticle); 104 | 105 | $ampImg = $document->createElement('amp-img'); 106 | $result = AMPCaption::create($instantArticle->getHeader()->getCover()->getCaption(), $context, $ampImg)->build(); 107 | $this->assertEquals($expected, $result->ownerDocument->saveXML($result)); 108 | } 109 | 110 | public function testCaptionOnTopOfElementBiggerSize() 111 | { 112 | $expected = 113 | '
'. 114 | '
Some caption to the image
'. 115 | ''. 116 | '
'; 117 | $instantArticle = $this->genInstantArticle(); 118 | $instantArticle->getHeader()->getCover()->getCaption()->withPosition(Caption::POSITION_ABOVE)->withFontsize(Caption::SIZE_XLARGE); 119 | 120 | $document = new \DOMDocument(); 121 | $context = $this->genContext($document, $instantArticle); 122 | 123 | $ampImg = $document->createElement('amp-img'); 124 | $result = AMPCaption::create($instantArticle->getHeader()->getCover()->getCaption(), $context, $ampImg)->build(); 125 | $this->assertEquals($expected, $result->ownerDocument->saveXML($result)); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/AMPCoverImageTest.php: -------------------------------------------------------------------------------- 1 | withHeader( 25 | Header::create() 26 | ->withTitle('Big Top Title') 27 | ->withSubTitle('Smaller SubTitle') 28 | ->withPublishTime( 29 | Time::create(Time::PUBLISHED) 30 | ->withDatetime( 31 | \DateTime::createFromFormat( 32 | 'j-M-Y G:i:s', 33 | '14-Aug-1984 19:30:00' 34 | ) 35 | ) 36 | ) 37 | ->addAuthor( 38 | Author::create() 39 | ->withName('Author One') 40 | ->withDescription('Passionate coder and mountain biker') 41 | ) 42 | ->addAuthor( 43 | Author::create() 44 | ->withName('Author Two') 45 | ->withDescription('Weend surfer with heavy weight coding skils') 46 | ->withURL('http://facebook.com/author') 47 | ) 48 | ->withKicker('Some kicker of this article') 49 | ->withCover( 50 | Image::create() 51 | ->withURL('http://blog.wod.expert/wp-content/uploads/2017/03/fail1.jpg') 52 | ) 53 | ); 54 | } 55 | 56 | private function genContext($document, $instantArticle) 57 | { 58 | $context = AMPContext::create($document, $instantArticle); 59 | 60 | $mediaSizes = array(); 61 | $mediaCacheFolder = __DIR__ . '/articles/media-cache'; 62 | $enableDownloadForMediaSizing = false; 63 | $defaultWidth = 1000; 64 | $defaultHeight = 900; 65 | 66 | $context->withMediaSizingSetup($mediaSizes, $mediaCacheFolder, $enableDownloadForMediaSizing, $defaultWidth, $defaultHeight); 67 | 68 | return $context; 69 | } 70 | 71 | public function testBuildCoverWithCaption() 72 | { 73 | $expected = 74 | '
'. 75 | '
'. 76 | ''. 77 | '
Some caption to the image
'. 78 | '
'. 79 | '
'; 80 | $instantArticle = $this->genInstantArticle(); 81 | $instantArticle->getHeader()->getCover()->withCaption( 82 | Caption::create() 83 | ->appendText('Some caption to the image') 84 | ); 85 | 86 | $document = new \DOMDocument(); 87 | $context = $this->genContext($document, $instantArticle); 88 | 89 | $result = AMPCoverImage::create($instantArticle->getHeader()->getCover(), $context, 'cover-image')->build(); 90 | $this->assertEquals($expected, $result->ownerDocument->saveXML($result)); 91 | } 92 | 93 | public function testBuildCoverWithoutCaption() 94 | { 95 | $expected = 96 | '
'. 97 | ''. 98 | '
'; 99 | $instantArticle = $this->genInstantArticle(); 100 | 101 | $document = new \DOMDocument(); 102 | $context = $this->genContext($document, $instantArticle); 103 | 104 | $result = AMPCoverImage::create($instantArticle->getHeader()->getCover(), $context, 'cover-image')->build(); 105 | $this->assertEquals($expected, $result->ownerDocument->saveXML($result)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/AMPHeaderTest.php: -------------------------------------------------------------------------------- 1 | logo = new AMPImage($url, $width, $height); 24 | } 25 | 26 | private function getRenderer($test, $customProperties = null) 27 | { 28 | $instantArticle = $this->loadInstantArticle(__DIR__ . '/articles/'.$test.'-instant-article.html'); 29 | $properties = array( 30 | 'lang' => 'en-US', 31 | AMPArticle::STYLES_FOLDER_KEY => __DIR__, 32 | AMPArticle::ENABLE_DOWNLOAD_FOR_MEDIA_SIZING_KEY => false, 33 | ); 34 | if (!is_null($customProperties)) { 35 | $properties = array_merge($properties, $customProperties); 36 | } 37 | 38 | return AMPArticle::create($instantArticle, $properties); 39 | } 40 | 41 | private function genContext() 42 | { 43 | $renderer = $this->getRenderer('test1', null); 44 | $context = AMPContext::create(new \DOMDocument('1.0'), $renderer->getInstantArticle()); 45 | 46 | $mediaSizes = array(); 47 | $mediaCacheFolder = __DIR__ . '/articles/media-cache'; 48 | $enableDownloadForMediaSizing = false; 49 | $defaultWidth = 1000; 50 | $defaultHeight = 900; 51 | 52 | $context->withMediaSizingSetup($mediaSizes, $mediaCacheFolder, $enableDownloadForMediaSizing, $defaultWidth, $defaultHeight); 53 | 54 | return $context; 55 | } 56 | 57 | private function genTestingHeader() 58 | { 59 | $context = $this->genContext(); 60 | 61 | $this->testHeader = new AMPHeader($context); 62 | $target = $this->testHeader->build(); 63 | $this->testHeader->genHeaderLogo($this->logo); 64 | 65 | $document = new \DOMDocument; 66 | $document->appendChild($document->importNode($target, true)); 67 | 68 | return new \DOMXPath($document); 69 | } 70 | 71 | public function testBuild() 72 | { 73 | $testingHeader = $this->genTestingHeader(); 74 | 75 | $publicationDateFetch = $testingHeader->query('//h3[@class="ia2amp-header-date"][1]')->item(0); 76 | $titleFetch = $testingHeader->query('//h1[@class="ia2amp-header-h1"][1]')->item(0); 77 | $byLineFetch = $testingHeader->query('//h3[@class="ia2amp-header-author"][1]')->item(0); 78 | $kickerFetch = $testingHeader->query('//h2[@class="ia2amp-header-category"][1]')->item(0); 79 | $logo = $testingHeader->query('//div[@class="ia2amp-header-bar-img-container"]/amp-img[1]')->item(0); 80 | 81 | $publicationDate = $publicationDateFetch->textContent; 82 | $title = $titleFetch->textContent; 83 | $byLine = $byLineFetch->textContent; 84 | $kicker = $kickerFetch->textContent; 85 | 86 | $this->assertEquals("http://blog.wod.expert/wp-content/uploads/2017/04/wod-expert-amp-org-logo.png", $logo->getAttribute("src")); 87 | $this->assertEquals(600, $logo->getAttribute("width")); 88 | $this->assertEquals(60, $logo->getAttribute("height")); 89 | $this->assertEquals("motivational", $kicker); 90 | $this->assertEquals("Very First WOD!", $title); 91 | //$this->assertEquals("May 10, 2016", $publicationDate); 92 | $this->assertEquals("By Éverton Rosário", $byLine); 93 | 94 | $spaceNodes = array( 95 | '//div[@class="ia2amp-header-bar"][1]', 96 | '//h2[@class="ia2amp-spacing after-header-date"][1]', 97 | '//h2[@class="ia2amp-spacing after-header-author before-header-date"][1]', 98 | '//div[@class="ia2amp-spacing after-header-category before-header-h1"][1]', 99 | '//div[@class="ia2amp-spacing after-header-h1 before-header-author"]', 100 | '//div[@class="ia2amp-spacing after-header-bar before-header-category"]', 101 | ); 102 | 103 | foreach ($spaceNodes as $currentNode) { 104 | $this->assertNotEmpty($testingHeader->query($currentNode)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/amp-converted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | Very First WOD! 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 |

motivational

25 |
26 |

Very First WOD!

27 |

BY Éverton Rosário

28 |

May 10, 2016

29 |
30 |
31 |
32 |

The first WOD we never forget! Just to be sure we are talking about same thing, WOD stands for “Workout of the Day”.  You feel you’re already gone on the warm up session.

33 |
“WOD stands for Workout of the Day”
34 |

Most likely you won’t be the only one in that training session, so it might be your first, but you will feel that others on your side are lightyears above and in front of you. You look at those faces, and they seem to be feeling nothing, but you are dying to run 100 meters sprint, unable to do 10 air squats and completely unable to perform 10 pushups.  One thing I gotta say to you: you’re not alone!

35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |

No matter which will be your very first workout. It can be one of the benchmark’s fancy named workouts or any unnamed workout, it doesn’t matter: you will “complete”, or better to say: you will suffer on it even just doing with the lightest weight on the box, using rubber, doing half of the repetitions, but still you will suck on performing it! Don’t feel ashamed of that!

46 |
47 | 48 |
49 |

Have you made thru it? What was your feeling by the end, even not finishing, or doing half of workout? You will pick one side here: Either you will get addicted and return every day, or you will just drop out. Which one you took?

50 |

I guess I already know your answer, once you are coming here!

51 |

Welcome, crossfitter!

52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/amp-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | Very First WOD! 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 27 |
28 | 29 |
30 | 31 |
32 |

Very First WOD!

33 | 37 |
38 | 40 |
41 |
42 | 43 |
44 |

45 | Flickr/crossfitpaleodietfitnessclassess

46 |
47 | 48 |
49 |

The first WOD we never forget! Just to be sure we are talking about same thing, WOD stands for “Workout of the Day”. You feel you’re already gone on the warm up session.

50 |

“WOD stands for Workout of the Day”

51 |

Most likely you won’t be the only one in that training session, so it might be your first, but you will feel that others on your side are lightyears above and in front of you. You look at those faces, and they seem to be feeling nothing, but you are dying to run 100 meters sprint, unable to do 10 air squats and completely unable to perform 10 pushups. One thing I gotta say to you: you’re not alone!

52 |

No matter which will be your very first workout. It can be one of the benchmark’s fancy named workouts or any unnamed workout, it doesn’t matter: you will “complete”, or better to say: you will suffer on it even just doing with the lightest weight on the box, using rubber, doing half of the repetitions, but still you will suck on performing it! Don’t feel ashamed of that!

53 |

54 |

Have you made thru it? What was your feeling by the end, even not finishing, or doing half of workout? You will pick one side here: Either you will get addicted and return every day, or you will just drop out. Which one you took?

55 |

I guess I already know your answer, once you are coming here!

56 |

Welcome, crossfitter!

57 |
58 | 59 | 64 | 65 |
66 | 67 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/media-cache/fail1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/facebook-instant-articles-sdk-extensions-in-php/05fb9556c77488c119ff1966db478dc11a95d787/tests/Facebook/InstantArticles/AMP/articles/media-cache/fail1.jpg -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/media-cache/fb_icon_325x325.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/facebook-instant-articles-sdk-extensions-in-php/05fb9556c77488c119ff1966db478dc11a95d787/tests/Facebook/InstantArticles/AMP/articles/media-cache/fb_icon_325x325.png -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/media-cache/heart-rate-age-300x182.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/facebook-instant-articles-sdk-extensions-in-php/05fb9556c77488c119ff1966db478dc11a95d787/tests/Facebook/InstantArticles/AMP/articles/media-cache/heart-rate-age-300x182.gif -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/media-cache/timelapse-final-4x3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/facebook-instant-articles-sdk-extensions-in-php/05fb9556c77488c119ff1966db478dc11a95d787/tests/Facebook/InstantArticles/AMP/articles/media-cache/timelapse-final-4x3.mp4 -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test1-amp-converted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | Very First WOD! 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 22 |
Flickr/crossfitpaleodietfitnessclassess
23 |
24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 |

motivational

32 |
33 |

Very First WOD!

34 |
35 |

It's great to see you here

36 |
37 |

By Éverton Rosário

38 |
39 |

May 10, 2016

40 |
41 |
42 |
43 |

The first WOD we never forget! Just to be sure we are talking about same thing, WOD stands for “Workout of the Day”.  You feel you’re already gone on the warm up session.

44 |
45 |
“WOD stands for Workout of the Day”
46 |
47 |

Most likely you won’t be the only one in that training session, so it might be your first, but you will feel that others on your side are lightyears above and in front of you. You look at those faces, and they seem to be feeling nothing, but you are dying to run 100 meters sprint, unable to do 10 air squats and completely unable to perform 10 pushups.  One thing I gotta say to you: you’re not alone!

48 |
49 |
50 | 51 |
52 |
53 | 54 |

55 | twopalswiththoughts 56 |

57 |
58 |
59 |
60 |
61 | 62 |

63 | Flickr: crossfitpaleodietfitnessclasses 64 |

65 |
66 |
67 |
68 |
69 |
70 |

No matter which will be your very first workout. It can be one of the benchmark’s fancy named workouts or any unnamed workout, it doesn’t matter: you will “complete”, or better to say: you will suffer on it even just doing with the lightest weight on the box, using rubber, doing half of the repetitions, but still you will suck on performing it! Don’t feel ashamed of that!

71 |
72 |
73 | 74 |
75 |
76 |

Have you made thru it? What was your feeling by the end, even not finishing, or doing half of workout? You will pick one side here: Either you will get addicted and return every day, or you will just drop out. Which one you took?

77 |
78 |

I guess I already know your answer, once you are coming here!

79 |
80 |

Welcome, crossfitter!

81 |
82 |
83 | 86 | © 2017 WOD Expert 87 |
88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test1-instant-article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 |
Flickr/crossfitpaleodietfitnessclassess
18 |
19 |

Very First WOD!

20 |

It's great to see you here

21 | 22 | 23 |
Éverton Rosário
24 |

motivational

25 |
26 |

The first WOD we never forget! Just to be sure we are talking about same thing, WOD stands for “Workout of the Day”.  You feel you’re already gone on the warm up session.

27 |
“WOD stands for Workout of the Day”
28 |

Most likely you won’t be the only one in that training session, so it might be your first, but you will feel that others on your side are lightyears above and in front of you. You look at those faces, and they seem to be feeling nothing, but you are dying to run 100 meters sprint, unable to do 10 air squats and completely unable to perform 10 pushups.  One thing I gotta say to you: you’re not alone!

29 |
30 |
31 | 32 |
33 | twopalswiththoughts 34 |
35 |
36 |
37 | 38 |
39 | Flickr: crossfitpaleodietfitnessclasses 40 |
41 |
42 |
43 |

No matter which will be your very first workout. It can be one of the benchmark’s fancy named workouts or any unnamed workout, it doesn’t matter: you will “complete”, or better to say: you will suffer on it even just doing with the lightest weight on the box, using rubber, doing half of the repetitions, but still you will suck on performing it! Don’t feel ashamed of that!

44 |
45 | 46 |
47 |

Have you made thru it? What was your feeling by the end, even not finishing, or doing half of workout? You will pick one side here: Either you will get addicted and return every day, or you will just drop out. Which one you took?

48 |

I guess I already know your answer, once you are coming here!

49 |

Welcome, crossfitter!

50 |
51 | 54 | © 2017 WOD Expert 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test2-instant-article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 |

Break your reps, before you die!

20 | 21 | 22 |
23 | Éverton Rosário 24 |
25 |

exercise

26 |
27 |

Having a good strategy to tackle the series in WODs are fundamental to achieve the best result given your particular physical condition. Comparing yourself to the top athletes, or trying to use their strategy probably won’t work well, unless you are a top athlete! By good strategy I mean planing basically how fast you will perform your movements and how you will play your pauses. There is no better person in the world to know you better than yourself.

28 |

But firstly you gotta understand better how your body responds to exercise and how you gotta plan given your body symptoms.

29 |

One of the three main areas to observe here are: heart rate, breathing and muscle energy.

30 |

Heart Rate

31 |

Heart rate is commonly measured in beats per minute and it measures how fast your heart is pumping blood to your body. When performing your exercises in the WOD your body is moving lots of muscles, so heart needs to keep oxygen level to your muscles, so it keeps working well.

32 |

Few characteristics influence how efficient you heart works, as more prepared you are, more efficient you heart works. Good to mention that you heart is also a muscle that by exercising in your WODs will also improve your heart condition. The heart beat at rest is one of good indicators on how well you are prepared, as top athletes have rest heart rate slightly slower than regular people.

33 |
34 | 35 |
The heart rate per age
36 |
37 |

Warning about heart rate is that, if in only few reps you get your heart rate close to maximum and this is going to be a long series. Example: your heart rate skyrocket getting close to the max at your age with only 5 reps, but the workout is about performing 50 reps, so you seriously need to review and lower the weights you will use.

38 |

During training you gotta keep close attention to your heart rate, and if you are getting into your red zone, you definitely will need a pause. But pause before you get there, otherwise it will be slower to recover once you are close to the max.

39 |

Using heart monitors will be a good aid by checking how your heart rating changes during your workout. As higher your heart goes, higher your breathing frequency goes

40 |

Breathing

41 |

During your series you will feel and observe yourself catching your breath in between the reps. This happens once you get closer to the max heart beat and you need to keep your oxygen levels in your body good enough so you keep contracting your muscles.

42 |

When you reach that respiration frequency, where it is too fast inhaling and exhaling, you will waste too much time catching your breath. Plan to break your workout few reps before you reach that level. This will make your recovering faster.

43 |

Once you reach this situation of inhale exhale to fast, and probably this will happen frequently, enforce yourself to exhale with more power, this will make your lungs go empty and inhaling more fresh air, thus, being more efficient about taking oxygen to your body, then your heart beat will slow down. Respiration frequency is really attached to your heart rate, and the influence occurs in both ways: heart beating influencing breathing frequency and vice-versa.

44 |

Muscle energy

45 |

By muscle energy I mean how ready your muscle is to perform one more contraction, with what power. To lift a weight or to perform the exercises during your WOD you will have lots of muscle contractions. Going deeper and understand what it takes for a muscle to contract, it will basically need to have your celular respiration and ATP working well. Just to remember it requires glucose, oxygen and strong muscle fibers. Once all requirements are met, you will successfully contract that muscle, thus executing the rep. Those three are fundamental.

46 |
47 | 48 |
49 |

Glucose -&gt; connected to your nutrition quality and how well you are eating before your workouts

50 |

Oxygen -&gt; How efficiency your breathing is and how well your heart rate is pumping the oxygen thru your blood.

51 |

Strong muscle fibers -&gt; That weight shouldn’t be heavier than your max for that movement.

52 |

As you exercise during your workout, you will feel your breathing go faster, and this is something you should try to avoid while contracting your muscles, because once you are contracting your muscles without the proper oxygen levels, you start the glycolysis process, which generates lactic acid causing your muscle fatigue.

53 |

All connected

54 |

As we can see, the 3 are all deeply connected. Getting more fitness prepared includes being stronger, good VO2 max and faster heart rating recovering. Being stronger will play one of the key differences here, since you will need less effort to perform the same exercise, thus keeping lower heart rate and breathing frequency. Once maybe you are not really strong you gotta keep checking your heart beat and breathing so you can stop before you get to the dying zone near your maximum. Once you get to your max, it is slower to recover.

55 |
56 | 57 |
58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test3-amp-converted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | Open 2017 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 |
CrossFit Games 2017 Logo
22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |

Open 2017

31 |
32 |

By Éverton Rosário

33 |
34 |

February 24, 2017

35 |
36 |
37 |
38 |

39 | The 40 | CrossFit Games 41 | are coming and the Open is the very first step for anyone try to fight for a spot on the regionals, then on the games! Pretty much almost all other sport modalities have a pre-selected group of athletes, teams or league affiliates. Sometimes the path is simply impossible: Try to get your football team to play on the NFL, it will be simply something near impossible, no matter how good that team is. Of course if your team is not part of the League you simply can’t. Not going into the merits of having a team on NFL, since this is not the point here. 42 |

43 |
44 |

What I’m claiming is that is really hard for most of the athletes, no matter the modality to get a spot on the highlight competition from that sport. This goes from olympic sports to any worldwide league.

45 |
46 |

In the CrossFit Games, there is no such a thing. And the Open is the very first step in, to fight for your spot to shine. It relies only on you and your results. Once you get your spot on the regionals, you are one step closer to the Games.

47 |
48 |

Let the Open begin! Here you will follow and have easy access to all content needed to follow up about the Open. Keep posted, since this is a dynamic post and will be regularly update once a week after each announcement and results pops out.

49 |
50 |

Are you competing? Please make sure you will survive, and good luck! 😉

51 |
52 |

17.1

53 |
54 |
55 |
56 | 57 |
17.1 Announcement
58 |
59 |
60 |
61 |

17.2

62 |
63 |
64 |
65 | 66 |
17.2 Announcement
67 |
68 |
69 |
70 |

17.3

71 |
72 |
73 |
74 | 75 |
17.3 Announcement
76 |
77 |
78 |
79 |

17.4

80 |
81 |
82 |
83 | 84 |
17.4 Announcement
85 |
86 |
87 |
88 |

17.5

89 |
90 |
91 |
92 | 93 |
17.5 Announcement
94 |
95 |
96 |
97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test3-instant-article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
CrossFit Games 2017 Logo
19 |
20 |

Open 2017

21 | 22 | 23 |
24 | Éverton Rosário 25 |
26 |
27 |

28 | The 29 | CrossFit Games 30 | are coming and the Open is the very first step for anyone try to fight for a spot on the regionals, then on the games! Pretty much almost all other sport modalities have a pre-selected group of athletes, teams or league affiliates. Sometimes the path is simply impossible: Try to get your football team to play on the NFL, it will be simply something near impossible, no matter how good that team is. Of course if your team is not part of the League you simply can’t. Not going into the merits of having a team on NFL, since this is not the point here. 31 |

32 |

What I’m claiming is that is really hard for most of the athletes, no matter the modality to get a spot on the highlight competition from that sport. This goes from olympic sports to any worldwide league.

33 |

In the CrossFit Games, there is no such a thing. And the Open is the very first step in, to fight for your spot to shine. It relies only on you and your results. Once you get your spot on the regionals, you are one step closer to the Games.

34 |

Let the Open begin! Here you will follow and have easy access to all content needed to follow up about the Open. Keep posted, since this is a dynamic post and will be regularly update once a week after each announcement and results pops out.

35 |

Are you competing? Please make sure you will survive, and good luck! 😉

36 |

17.1

37 |
38 | 41 |
17.1 Announcement
42 |
43 |

17.2

44 |
45 | 48 |
17.2 Announcement
49 |
50 |

17.3

51 |
52 | 55 |
17.3 Announcement
56 |
57 |

17.4

58 |
59 | 62 |
17.4 Announcement
63 |
64 |

17.5

65 |
66 | 69 |
17.5 Announcement
70 |
71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test4-instant-article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
19 |

DU – Damm U (Double Unders)!

20 | 21 | 22 |
23 | Éverton Rosário 24 |
25 |

double under

26 |
27 |

Damm you Double Unders (DU)! I guess lots of people will agree with the DAMM part… But why the Double Unders are so problematic since it is just a simple jumping rope? Well, maybe not that simple! 😉

28 |
29 | 30 |
@MrWhaite
31 |
32 |

Jumping rope might be simple as most people were used to do it during school years. And by jumping rope I mean the single unders.

33 |
34 | “A 35 | double under 36 | is a popular exercise done on a 37 | jump rope 38 | in which the rope makes two passes per jump instead of just one.” 39 |
40 | Athlepedia 41 |
42 |

One of the key characteristics comparing the double unders to the single unders are 2:

43 |
    44 |
  • the speed you spin the rope; and
  • 45 |
  • how high the jump is.
  • 46 |
47 |

In simple math therms, if you jump twice as high from your single under would be enough to perfectly accomplish the double unders. Another possibility is spinning twice as fast the rope, thus not needing to change how high you jump. The actual answer here is that none of the situations will be the correct answer, otherwise you will burn your wrists and your handle power, or you will burn your legs more than necessary. Combining the DU into a more complex WOD will make these 2 initial extreme options even more useless, since probably you will have any other exercise that will demand your handle or your legs, or even worst: both.

48 |
49 | 50 |
51 |

Progression

52 |

A good progression to perform your double unders if you still didn’t make it is to start without any rope! “Are you crazy?” you might be wondering. But no, Im not crazy or out of my mind. The point to start here is to start jumping consistently on your toes, not on your hills, and bending minimally as possible your knees. Once you are jumping consistently high, and by high, try jumping twice as high of your single under, or maybe even higher if you can. Once you can perform at least 10 sequence jumps high enough with not much difference between the jumps. At this high you can perform the “double tap” on your hips using your free hands (remember, you are not using hands so far). Also remember to jump straight, without high knees nor rising your feet. Once you completed all these steps, you are good to move forward.

53 |

You will start using a rope, consider buying one for you or make sure you will have yours reserved every time on your box. Otherwise mixing different ropes with different specs you make this process more difficult.

54 |

Now it is time to start using a rope, but still, it is not time to try to perform the double under. You need to keep jumping as high as the previous step, but performing only a single under. You will notice that you will be jumping really high and will need to slow down a lot the rope speed, otherwise you will be landing and the rope will be arriving at your feet at same time. Slow down and get used to the high jumps. Try to keep as high as you did before without the rope. Once you perform 10 unbroken high jumps with slow rope speed, is time to move forward.

55 |

Now you will be jumping as high as you did in both previous steps, but now you will speed up your rope trying to perform the double under. Performing these steps will raise your odds to accomplish the regular double unders.

56 |

Once you start performing more regularly your double unders, is practice time. And by practice I mean: exhaustive practicing. Try to perform at least 200 double unders. This will definitely make you get more feeling and understand your body balance, jumping and wrist skills.

57 |

The hidden characteristics of Double Unders

58 |

Double unders are hard to get, but once you get used to it, you will perform it really smoothly. By looking to a more complex WOD where you have DU in it, you might underestimate how heavy the DU’s might be because it will:

59 |
    60 |
  • burn your wrists/handle
  • 61 |
  • exhaust your legs
  • 62 |
  • raise/keep high your heart beat
  • 63 |
64 |

These three might sound inoffensive but don’t mis-judge this. They will make you suffer, suffer hard depending on the WOD. If the WOD mixes some hang moves, pull ups, muscle ups or any other exercise that burns your grip, the rope might feel harder to spin. This will be the perfect timing to jump a little higher and save your wrists a little bit. On the other hand, if your WOD is really burning your legs, might be a good opportunity to save some energies by jumping little lower and spinning faster. If your WOD burns your grip + legs, try to keep a balance with the 50-50 strategy, and know that by the end of this WOD you’ll be supplicating for your life, I mean, your breath! 😛

65 |

Hints for warming up

66 |
    67 |
  • Perform sets of single unders
  • 68 |
  • Run some short sprints (100m is enough)
  • 69 |
  • Few box jump
  • 70 |
71 |
72 | 73 | 74 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test5-amp-converted.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | Wall Ball or Ball Shot? 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 |
Rich Froning at Wall Ball Shot
22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |

exercise

31 |
32 |

Wall Ball or Ball Shot?

33 |
34 |

By Éverton Rosário

35 |
36 |

May 20, 2016

37 |
38 |
39 |
40 |

41 | Both expressions seems correct and in common shares the requisite of 42 | breaking parallel line 43 | while squatting. Just for correctness, the Wall Ball Shot is the best title to use. 44 |

45 |
46 |

One thing that comes to mind is that when thinking about wall ball It reminds me of only one constraint to make it a valid movement. So if you throw the ball below the limit line it is a “no-rep”, otherwise +1 to your reps. 😉

47 |
48 |

Looking to the wall ball shot perspective, not only the bottom limit is important, but also there is a top, left and right limits. In other words: you gotta hit the target.

49 |
50 |
51 |
52 | 53 |
Target used on most of competitions
54 |
55 |
56 |
57 |

CrossFit isn’t just about being strong and fast, but also precise. Always consider yourself doing your wall balls hitting at same target always. If you compete into any fitness competition, the odds of using a ball shot exercise increases, since the ball shot are easier equipment to have on open spaces. The wall, save for your box 😉

58 |
59 |

Hints for the exercise

60 |
61 |

Never stop looking at your target, the piece of metal, circle, line or the imaginary “X” mark on the wall. This will make your body foresee where you need to hit, and just throw there, you’ll gonna hit it!

62 |
63 |

64 | Never forget to break the parallel, and while going down, don’t try to slow your descent, this will make you burn your muscles. Always remember, if you want to perform better on the WODs 65 | always think about saving energy 66 | . 67 |

68 |
69 |

Thinking about doing the wallball shots, never forget to use all your body as lever to accomplish the movement with the minimal effort possible. By this I mean:

70 |
71 |
    72 |
  1. Start movement by performing a “deadlift”getting the ball from floor
  2. 73 |
  3. Go down as fast as you can, holding the ball at your face level, holding by the bottom of the ball
  4. 74 |
  5. Break the parallel and then explode as fast as you can your legs
  6. 75 |
  7. By the moment you are straight up, your ball will move up without any force
  8. 76 |
  9. Push the ball up, like setting a volleyball ball, never forgetting to use your wrists, this will save some shoulders and legs energy
  10. 77 |
  11. You HIT the target! Add one to your counting sequence, never forget this 😉 !
  12. 78 |
  13. If your counting comes to the correct number, jump to number 11.
  14. 79 |
  15. The ball will start to descend, keep your arms straight up waiting for the contact
  16. 80 |
  17. Absorb the ball with your arms and wrists and start the descend, using the less amount of energy possible
  18. 81 |
  19. Repeat from step 2!
  20. 82 |
  21. You are done!
  22. 83 |
84 |
85 |
86 | 87 |
88 |
89 | 90 |

Squat preparing to throw

91 |
92 |
93 |
94 |
95 | 96 |

The wall ball throw using the wrists power, just like volleyball setting

97 |
98 |
99 |
100 |
101 |
102 |

Hints for warming up

103 |
104 |
    105 |
  • Perform sets of air squats
  • 106 |
  • Use lighter med ball to perform few shots
  • 107 |
  • Perform some sets of walking lunges
  • 108 |
109 |
110 |
111 | 112 | 113 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/articles/test5-instant-article.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 |
Rich Froning at Wall Ball Shot
19 |
20 |

Wall Ball or Ball Shot?

21 | 22 | 23 |
24 | Éverton Rosário 25 |
26 |

exercise

27 |
28 |

29 | Both expressions seems correct and in common shares the requisite of 30 | breaking parallel line 31 | while squatting. Just for correctness, the Wall Ball Shot is the best title to use. 32 |

33 |

One thing that comes to mind is that when thinking about wall ball It reminds me of only one constraint to make it a valid movement. So if you throw the ball below the limit line it is a “no-rep”, otherwise +1 to your reps. 😉

34 |

Looking to the wall ball shot perspective, not only the bottom limit is important, but also there is a top, left and right limits. In other words: you gotta hit the target.

35 |
36 | 37 |
Target used on most of competitions
38 |
39 |

CrossFit isn’t just about being strong and fast, but also precise. Always consider yourself doing your wall balls hitting at same target always. If you compete into any fitness competition, the odds of using a ball shot exercise increases, since the ball shot are easier equipment to have on open spaces. The wall, save for your box 😉

40 |

Hints for the exercise

41 |

Never stop looking at your target, the piece of metal, circle, line or the imaginary “X” mark on the wall. This will make your body foresee where you need to hit, and just throw there, you’ll gonna hit it!

42 |

43 | Never forget to break the parallel, and while going down, don’t try to slow your descent, this will make you burn your muscles. Always remember, if you want to perform better on the WODs 44 | always think about saving energy 45 | . 46 |

47 |

Thinking about doing the wallball shots, never forget to use all your body as lever to accomplish the movement with the minimal effort possible. By this I mean:

48 |
    49 |
  1. Start movement by performing a “deadlift”getting the ball from floor
  2. 50 |
  3. Go down as fast as you can, holding the ball at your face level, holding by the bottom of the ball
  4. 51 |
  5. Break the parallel and then explode as fast as you can your legs
  6. 52 |
  7. By the moment you are straight up, your ball will move up without any force
  8. 53 |
  9. Push the ball up, like setting a volleyball ball, never forgetting to use your wrists, this will save some shoulders and legs energy
  10. 54 |
  11. You HIT the target! Add one to your counting sequence, never forget this 😉 !
  12. 55 |
  13. If your counting comes to the correct number, jump to number 11.
  14. 56 |
  15. The ball will start to descend, keep your arms straight up waiting for the contact
  16. 57 |
  17. Absorb the ball with your arms and wrists and start the descend, using the less amount of energy possible
  18. 58 |
  19. Repeat from step 2!
  20. 59 |
  21. You are done!
  22. 60 |
61 |
62 |
63 | 64 |
Squat preparing to throw
65 |
66 |
67 | 68 |
The wall ball throw using the wrists power, just like volleyball setting
69 |
70 |
71 |

Hints for warming up

72 |
    73 |
  • Perform sets of air squats
  • 74 |
  • Use lighter med ball to perform few shots
  • 75 |
  • Perform some sets of walking lunges
  • 76 |
77 |

78 |

79 | 80 | 81 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/AMP/default.amp-custom.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. */ 2 | /* 3 | Custom CSS for default style goes here 4 | */ 5 | 6 | /* 7 | p { 8 | background-color: rgb(255,00,00); 9 | } 10 | */ 11 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Utils/CSSBuilderTest.php: -------------------------------------------------------------------------------- 1 | genSimpleCSS(); 82 | 83 | $cssBuilder = new CSSBuilder(); 84 | $cssBuilder->addProperty('.someClass', 'width', '300px') 85 | ->addProperty('.someClass', 'height', '400px'); 86 | $result = $cssBuilder->build(false); 87 | $this->assertEquals($expected, $result); 88 | } 89 | 90 | public function testSimpleCSSFormatted() 91 | { 92 | $expected = $this->genSimpleCSSFormatted(); 93 | 94 | $cssBuilder = new CSSBuilder(''); 95 | $cssBuilder->addProperty('.someClass', 'width', '300px') 96 | ->addProperty('.someClass', 'height', '400px'); 97 | $result = $cssBuilder->build(true); 98 | $this->assertEquals($expected, $result); 99 | } 100 | 101 | public function testMultipleCSSFormatted() 102 | { 103 | $expected = $this->genSimpleCSSFormatted()."\n\n".$this->genOtherCSSFormatted(); 104 | 105 | $cssBuilder = new CSSBuilder(''); 106 | $cssBuilder->addProperty('.someClass', 'width', '300px') 107 | ->addProperty('.someClass', 'height', '400px') 108 | ->addProperty('.otherClass', 'background-color', '#aabbcc') 109 | ->addProperty('.otherClass', 'border-width', '2px'); 110 | $result = $cssBuilder->build(true); 111 | $this->assertEquals($expected, $result); 112 | } 113 | 114 | public function testMultipleCSSFormattedWithPrefix() 115 | { 116 | $expected = $this->genSimpleCSSFormatted('myprefix-'); 117 | 118 | $cssBuilder = new CSSBuilder('myprefix-'); 119 | $cssBuilder->addToSelector('someClass', 'width', '300px') 120 | ->addToSelector('someClass', 'height', '400px'); 121 | $result = $cssBuilder->build(true); 122 | $this->assertEquals($expected, $result); 123 | } 124 | 125 | public function testMultipleDimmensionCSSFormattedWithPrefix() 126 | { 127 | $expected = $this->genSimpleCSSFormatted('myprefix-'); 128 | 129 | $cssBuilder = new CSSBuilder('myprefix-'); 130 | $cssBuilder->addDimensionToSelector('someClass', 'width', '300', 'px') 131 | ->addDimensionToSelector('someClass', 'height', '400', 'px'); 132 | $result = $cssBuilder->build(true); 133 | $this->assertEquals($expected, $result); 134 | } 135 | 136 | public function testSpacingCSSFormattedWithPrefix() 137 | { 138 | $expected = $this->genSpacingCSSFormatted('myprefix-'); 139 | 140 | $cssBuilder = new CSSBuilder('myprefix-'); 141 | $cssBuilder->addDimensionToSelector('someClass', 'width', '300', 'px') 142 | ->addDimensionToSelector('someClass', 'height', '400', 'px') 143 | ->addHeightSpacingToSelector('someClass', '18'); 144 | $result = $cssBuilder->build(true); 145 | $this->assertEquals($expected, $result); 146 | } 147 | 148 | public function testTopRightBottomLeftCSSFormattedWithPrefix() 149 | { 150 | $expected = $this->genMarginCSSFormatted('myprefix-'); 151 | 152 | $cssBuilder = new CSSBuilder('myprefix-'); 153 | $cssBuilder->addTopRightBottomLeftToSelector('someClass', 'margin', null, '10', 5, 0, 'px') 154 | ->addDimensionToSelector('someClass', 'height', '400', 'px'); 155 | $result = $cssBuilder->build(true); 156 | $this->assertEquals($expected, $result); 157 | } 158 | 159 | public function testArrayCSSFormattedWithPrefix() 160 | { 161 | $expected = $this->genArrayCSSFormatted('myprefix-'); 162 | 163 | $cssBuilder = new CSSBuilder('myprefix-'); 164 | $cssBuilder->addToSelector(array('someClass1', 'someClass2'), 'width', '300px') 165 | ->addToSelector(array('someClass1', 'someClass2'), 'height', '400px') 166 | ->addToSelector('someClass1', 'color', '#fff'); 167 | $result = $cssBuilder->build(true); 168 | $this->assertEquals($expected, $result); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Utils/FileUtilsPHPUnitTestCase.php: -------------------------------------------------------------------------------- 1 | loadHTMLFile($file, $encoding); 32 | return $this->loadDOMDocumentFromString($fileContent, $encoding); 33 | } 34 | 35 | /** 36 | * Helper method that loads HTML file into DOMDocument instance, encoding as HTML-ENTITIES and using by default utf-8. 37 | * @param string $fileContent The file content 38 | * @param string $encoding "utf-8" by default. Supports the format informed. 39 | */ 40 | public function loadDOMDocumentFromString($fileContent, $encoding = 'utf-8') 41 | { 42 | libxml_use_internal_errors(true); 43 | $document = new \DOMDocument('1.0'); 44 | $document->loadHTML(''.$fileContent); 45 | libxml_use_internal_errors(false); 46 | return $document; 47 | } 48 | 49 | /** 50 | * Helper method that will load file, parse as Instant Article and return the element. 51 | * @param string $file File name to be loaded/parsed as InstantArticle. 52 | * @param string $encoding "utf-8" by default. Supports and loads the instant article treating file with the informed encoding. 53 | */ 54 | public function loadInstantArticle($file, $encoding = 'utf-8') 55 | { 56 | $document = $this->loadDOMDocument($file, $encoding); 57 | $parser = new Parser(); 58 | return $parser->parse($document); 59 | } 60 | 61 | protected function assertEqualsHtml($expected, $actual) 62 | { 63 | $from = ['/\>[^\S ]+/s', '/[^\S ]+\ ', '<', '\\1', '><']; 65 | $this->assertEquals( 66 | preg_replace($from, $to, $expected), 67 | preg_replace($from, $to, $actual) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Utils/Greeting.php: -------------------------------------------------------------------------------- 1 | greeting = $greeting; 21 | } 22 | 23 | /** 24 | * Says hello to someone 25 | */ 26 | public static function hello($name) 27 | { 28 | return "Hello $name"; 29 | } 30 | 31 | /** 32 | * Method that returns a string greeting someone 33 | */ 34 | public function greet($name, $middleName = null, $lastName = null) 35 | { 36 | $name = ($this->greeting).' '.$name; 37 | if ($middleName) { 38 | $name = $name.' '.$middleName; 39 | } 40 | if ($lastName) { 41 | $name = $name.' '.$lastName; 42 | } 43 | return $name; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Utils/HookTest.php: -------------------------------------------------------------------------------- 1 | first = '1st'; 34 | } 35 | 36 | public function methodHookTestingBefore($objToChange) 37 | { 38 | return 'MAIN-WITH-BEFORE-'.$objToChange->first.'-'.$objToChange->second; 39 | } 40 | 41 | public function methodHookAfter2($objToChange) 42 | { 43 | $objToChange->second = '2nd'; 44 | } 45 | 46 | public function methodHookTestingAfter($objToChange) 47 | { 48 | return 'MAIN-WITH-AFTER-'.$objToChange->first.'-'.$objToChange->second; 49 | } 50 | 51 | public static function staticMethodHookParams($arg1, $arg2) 52 | { 53 | return 'STATIC-with-params-'.$arg1.'-'.$arg2; 54 | } 55 | 56 | public function testHookDefault() 57 | { 58 | $hook = Hook::create(); 59 | $result = $hook->call('hook_name', array($this, 'methodHook')); 60 | $this->assertEquals('result', $result); 61 | } 62 | 63 | public function testHookReplacingDefault() 64 | { 65 | $hook = Hook::create(); 66 | $hook->setHook('hook_name', array($this, 'methodHookReplacing')); 67 | $result = $hook->call('hook_name', array($this, 'methodHook')); 68 | $this->assertEquals('REPLACED', $result); 69 | } 70 | 71 | public function testHookReplacingDefaultRemoved() 72 | { 73 | $hook = Hook::create(); 74 | $hook->setHook('hook_name', array($this, 'methodHookReplacing')); 75 | $result = $hook->call('hook_name', array($this, 'methodHook')); 76 | $this->assertEquals('REPLACED', $result); 77 | 78 | $hook->removeHook('hook_name'); 79 | $result = $hook->call('hook_name', array($this, 'methodHook')); 80 | $this->assertEquals('result', $result); 81 | } 82 | 83 | public function testHookRemovingSomethingNeverAdded() 84 | { 85 | $hook = Hook::create(); 86 | $result = $hook->call('hook_name', array($this, 'methodHook')); 87 | $this->assertEquals('result', $result); 88 | 89 | $hook->removeHook('hook_name'); 90 | $result = $hook->call('hook_name', array($this, 'methodHook')); 91 | $this->assertEquals('result', $result); 92 | } 93 | 94 | public function testHookDefaultBefore() 95 | { 96 | $hook = Hook::create(); 97 | $objToChange = new \StdClass; 98 | $objToChange->first = '1'; 99 | $objToChange->second = '2'; 100 | 101 | // Calls method without overriding the value, so the return should be 1 102 | $objToChange->first = '1'; 103 | $objToChange->second = '2'; 104 | $result = $hook->call('hook_name', array($this, 'methodHookTestingBefore'), array($objToChange)); 105 | $this->assertEquals('MAIN-WITH-BEFORE-1-2', $result); 106 | 107 | // Calls method overriding the value, so the return should be 1st 108 | $objToChange->first = '1'; 109 | $objToChange->second = '2'; 110 | $hook->setBeforeHook('hook_name', array($this, 'methodHookBefore1'), array($objToChange)); 111 | $result = $hook->call('hook_name', array($this, 'methodHookTestingBefore'), array($objToChange)); 112 | $this->assertEquals('MAIN-WITH-BEFORE-1st-2', $result); 113 | } 114 | 115 | public function testHookDefaultAfter() 116 | { 117 | $hook = Hook::create(); 118 | $objToChange = new \StdClass; 119 | $objToChange->first = '1'; 120 | $objToChange->second = '2'; 121 | 122 | // Calls method without overriding the value, so the return should be 1 123 | $objToChange->first = '1'; 124 | $objToChange->second = '2'; 125 | $result = $hook->call('hook_name', array($this, 'methodHookTestingAfter'), array($objToChange)); 126 | $this->assertEquals('MAIN-WITH-AFTER-1-2', $result); 127 | 128 | // Calls method overriding the value, so the return should be 2nd 129 | $objToChange->first = '1'; 130 | $objToChange->second = '2'; 131 | $hook->setAfterHook('hook_name', array($this, 'methodHookAfter2'), array($objToChange)); 132 | $result = $hook->call('hook_name', array($this, 'methodHookTestingAfter'), array($objToChange)); 133 | $this->assertEquals('MAIN-WITH-AFTER-1-2', $result); 134 | $this->assertEquals('2nd', $objToChange->second); 135 | } 136 | 137 | public function testHookDefaultBeforeAndAfter() 138 | { 139 | $hook = Hook::create(); 140 | $objToChange = new \StdClass; 141 | $objToChange->first = '1'; 142 | $objToChange->second = '2'; 143 | 144 | // Calls method without overriding the value, so the return should be 1 145 | $objToChange->first = '1'; 146 | $objToChange->second = '2'; 147 | $result = $hook->call('hook_name', array($this, 'methodHookTestingBefore'), array($objToChange)); 148 | $this->assertEquals('MAIN-WITH-BEFORE-1-2', $result); 149 | 150 | // Calls method overriding the value, so the return should be 1st 151 | $objToChange->first = '1'; 152 | $objToChange->second = '2'; 153 | $hook->setBeforeHook('hook_name', array($this, 'methodHookBefore1'), array($objToChange)); 154 | $result = $hook->call('hook_name', array($this, 'methodHookTestingBefore'), array($objToChange)); 155 | $this->assertEquals('MAIN-WITH-BEFORE-1st-2', $result); 156 | 157 | // Calls method overriding the value, so the return should be 2nd 158 | $objToChange->first = '1'; 159 | $objToChange->second = '2'; 160 | $hook->setAfterHook('hook_name', array($this, 'methodHookAfter2'), array($objToChange)); 161 | $result = $hook->call('hook_name', array($this, 'methodHookTestingAfter'), array($objToChange)); 162 | $this->assertEquals('MAIN-WITH-AFTER-1st-2', $result); 163 | $this->assertEquals('2nd', $objToChange->second); 164 | } 165 | 166 | public function testHookDefaultBeforeAndAfterRemoved() 167 | { 168 | $hook = Hook::create(); 169 | $objToChange = new \StdClass; 170 | 171 | // Calls method without overriding the value, so the return should be 1 172 | $objToChange->first = '1'; 173 | $objToChange->second = '2'; 174 | $hook->setBeforeHook('hook_name', array($this, 'methodHookBefore1'), array($objToChange)); 175 | $hook->setAfterHook('hook_name', array($this, 'methodHookAfter2'), array($objToChange)); 176 | $result = $hook->call('hook_name', array($this, 'methodHookTestingAfter'), array($objToChange)); 177 | $this->assertEquals('MAIN-WITH-AFTER-1st-2', $result); 178 | $this->assertEquals('2nd', $objToChange->second); 179 | 180 | $objToChange->first = '1'; 181 | $objToChange->second = '2'; 182 | $hook->clearHooks('hook_name'); 183 | $result = $hook->call('hook_name', array($this, 'methodHookTestingBefore'), array($objToChange)); 184 | $this->assertEquals('MAIN-WITH-BEFORE-1-2', $result); 185 | } 186 | 187 | public function testHookReplacingDefaultParams() 188 | { 189 | $hook = Hook::create(); 190 | $hook->setHook('hook_name', array($this, 'methodHookReplacing'), array('param1', 'param2')); 191 | $result = $hook->call('hook_name', array($this, 'methodHook')); 192 | $this->assertEquals('REPLACED-param1-param2', $result); 193 | } 194 | 195 | public function testHookStaticMethod() 196 | { 197 | $hook = Hook::create(); 198 | $result = $hook->call('hook_name', array('Facebook\InstantArticles\Utils\HookTest', 'staticMethodHook')); 199 | $this->assertEquals('STATIC', $result); 200 | } 201 | 202 | public function testHookStaticMethodParams() 203 | { 204 | $hook = Hook::create(); 205 | $result = $hook->call('hook_name', array('Facebook\InstantArticles\Utils\HookTest', 'staticMethodHookParams'), array('param1', 'param2')); 206 | $this->assertEquals('STATIC-with-params-param1-param2', $result); 207 | } 208 | 209 | public function testHookFunctionNoClass() 210 | { 211 | $hook = Hook::create(); 212 | $result = $hook->call('hook_name', 'Facebook\InstantArticles\Utils\functionOutsideClass'); 213 | $this->assertEquals('OUTSIDER', $result); 214 | } 215 | 216 | public function testHookFunctionNoClassWithParam() 217 | { 218 | $hook = Hook::create(); 219 | $result = $hook->call('hook_name', 'Facebook\InstantArticles\Utils\functionOutsideClassWithParams', array('param1')); 220 | $this->assertEquals('OUTSIDER-with-param-param1', $result); 221 | } 222 | } 223 | 224 | function functionOutsideClass() 225 | { 226 | return 'OUTSIDER'; 227 | } 228 | 229 | function functionOutsideClassWithParams($arg1) 230 | { 231 | return 'OUTSIDER-with-param-'.$arg1; 232 | } 233 | -------------------------------------------------------------------------------- /tests/Facebook/InstantArticles/Utils/ObserverTest.php: -------------------------------------------------------------------------------- 1 | applyFilters('name', "Bob"); 19 | $this->assertEquals('Bob', $name); 20 | } 21 | 22 | public function testSingleFilter() 23 | { 24 | $observer = Observer::create(); 25 | $observer->addFilter('name', function ($name) { 26 | return "$name-san"; 27 | }); 28 | $name = $observer->applyFilters('name', "Bob"); 29 | $this->assertEquals('Bob-san', $name); 30 | } 31 | 32 | public function testStaticFilter() 33 | { 34 | $observer = Observer::create(); 35 | $observer->addFilter('name', array("Facebook\InstantArticles\Utils\Greeting", "hello")); 36 | 37 | $name = $observer->applyFilters('name', "Bob"); 38 | $this->assertEquals('Hello Bob', $name); 39 | } 40 | 41 | public function testFilterWithReferences() 42 | { 43 | $mr = new Greeting("Mr."); 44 | $hello = new Greeting("Hello"); 45 | 46 | $observer = Observer::create(); 47 | $observer->addFilter('name', array(&$mr, "greet")); 48 | $observer->addFilter('name', array(&$hello, "greet")); 49 | 50 | $name = $observer->applyFilters('name', "Bob"); 51 | $this->assertEquals('Hello Mr. Bob', $name); 52 | } 53 | 54 | public function testFilterWithParameters() 55 | { 56 | $hello = new Greeting("Hello"); 57 | 58 | $observer = Observer::create(); 59 | $observer->addFilter('name', array(&$hello, "greet"), 10, 4); 60 | 61 | $name = $observer->applyFilters('name', "Spongebob", "Square", "Pants"); 62 | $this->assertEquals('Hello Spongebob Square Pants', $name); 63 | } 64 | 65 | public function testFilterWithReferencesAndParameters() 66 | { 67 | $hello = new Greeting("Hello"); 68 | $mr = new Greeting("Mr."); 69 | 70 | $observer = Observer::create(); 71 | $observer->addFilter('name', array(&$hello, "greet"), 11, 2); 72 | $observer->addFilter('name', array(&$mr, "greet"), 10, 1); 73 | 74 | $name = $observer->applyFilters('name', "Bob", "Bobby"); 75 | $this->assertEquals('Hello Mr. Bob Bobby', $name); 76 | } 77 | 78 | public function testRemoveFilterWithReferencesParameters() 79 | { 80 | $hello = new Greeting("Hello"); 81 | $mr = new Greeting("Mr."); 82 | 83 | $observer = Observer::create(); 84 | $observer->addFilter('name', array(&$hello, "greet"), 11, 2); 85 | $observer->addFilter('name', array(&$mr, "greet"), 10, 1); 86 | 87 | $name = $observer->applyFilters('name', "Bob", "Bobby"); 88 | $this->assertEquals('Hello Mr. Bob Bobby', $name); 89 | 90 | $observer->removeFilter('name', array(&$mr, "greet"), 10); 91 | 92 | $name = $observer->applyFilters('name', "Bob", "Bobby"); 93 | $this->assertEquals('Hello Bob Bobby', $name); 94 | } 95 | 96 | public function testRemoveAllFiltersWithReferencesAndParameters() 97 | { 98 | $hello = new Greeting("Hello"); 99 | $mr = new Greeting("Mr."); 100 | 101 | $observer = Observer::create(); 102 | $observer->addFilter('name', array(&$hello, "greet"), 11, 2); 103 | $observer->addFilter('name', array(&$mr, "greet"), 10, 1); 104 | 105 | $name = $observer->applyFilters('name', "Bob", "Bobby"); 106 | $this->assertEquals('Hello Mr. Bob Bobby', $name); 107 | 108 | $observer->removeAllFilters('name', 11); 109 | 110 | $name = $observer->applyFilters('name', "Bob", "Bobby"); 111 | $this->assertEquals('Mr. Bob', $name); 112 | 113 | $observer->removeAllFilters('name'); 114 | 115 | $name = $observer->applyFilters('name', "Bob", "Bobby"); 116 | $this->assertEquals('Bob', $name); 117 | } 118 | 119 | public function testHasFilter() 120 | { 121 | $hello = new Greeting("Hello"); 122 | $mr = new Greeting("Mr."); 123 | $mrs = new Greeting("Mrs."); 124 | 125 | $observer = Observer::create(); 126 | $observer->addFilter('name', array(&$hello, "greet"), 11, 2); 127 | $observer->addFilter('name', array(&$mr, "greet"), 10, 1); 128 | 129 | $this->assertEquals(11, $observer->hasFilter('name', array(&$hello, "greet"))); 130 | $this->assertEquals(10, $observer->hasFilter('name', array(&$mr, "greet"))); 131 | $this->assertFalse($observer->hasFilter('name', array(&$mrs, "greet"))); 132 | $this->assertTrue($observer->hasFilter('name')); 133 | } 134 | } 135 | --------------------------------------------------------------------------------