├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── composer.lock ├── phpspec.yml ├── spec ├── AmpConverterSpec.php ├── Converter │ ├── ConverterInterface.php │ ├── Extensions │ │ ├── InstagramConverterSpec.php │ │ └── YoutubeConverterSpec.php │ └── ImageConverterSpec.php ├── ElementSpec.php └── EnvironmentSpec.php └── src ├── AmpConverter.php ├── Converter ├── ConverterInterface.php ├── Extensions │ ├── CloudinaryImageConverter.php │ ├── InstagramConverter.php │ ├── TwitterConverter.php │ └── YoutubeConverter.php └── ImageConverter.php ├── Element.php ├── ElementInterface.php └── Environment.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '5.5' 4 | - '5.6' 5 | - '7.0' 6 | 7 | before_install: 8 | - composer self-update 9 | 10 | install: 11 | - composer install --prefer-source --no-interaction 12 | 13 | script: vendor/bin/phpspec run -v --format progress 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML to AMP HTML 2 | A PHP Library to Convert HTML to [AMP HTML](https://www.ampproject.org/) 3 | 4 | **This is a new library and not considered stable yet.** 5 | 6 | ### Documentation 7 | 8 | See the [wiki](https://github.com/paulredmond/html-to-amp/wiki). Documentation is a WIP right now. 9 | 10 | ### Why? 11 | 12 | I have a need to convert HTML content into valid AMP (a subset of HTML) format from disparate CMS systems in a (hopefully) somewhat consistent manner. Instead of hacking regular expressions and doing the same thing over and over again, I thought it would be useful to create a library to parse HTML and convert it into AMP HTML. 13 | 14 | The library's **main purpose is parsing fragments of HTML content (an article body)** and converting it into valid AMP. At this point you could try to parse an entire HTML document, but that's not this library's sweet spot. Pull requests are welcomed of course if you find a need; [open an issue](https://github.com/paulredmond/html-to-amp/issues) and discuss your ideas before submitting a PR so I can understand your goals/needs and align them to this project. 15 | 16 | ### Design Goals 17 | 18 | * Make it simple, yet extensible 19 | * Convert and Replace Elements in a passive way 20 | * Allow client code to configure which conversions run 21 | * Allow client code to add custom conversions as needed 22 | 23 | ### Inspiration 24 | 25 | This Library was inspired by code design patterns found in the wonderful [thephpleague/html-to-markdown](https://github.com/thephpleague/html-to-markdown) PHP library. Generally, the objectives in this Library are similiar: parse HTML into another format—in this case a subset of HTML. 26 | 27 | ### Testing 28 | 29 | [![Build Status](https://travis-ci.org/paulredmond/html-to-amp.svg?branch=master)](https://travis-ci.org/paulredmond/html-to-amp) 30 | 31 | ``` 32 | $ vendor/bin/phpspec run 33 | ``` 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paulredmond/html-to-amp", 3 | "type": "library", 4 | "keywords": ["amp", "amp-html", "html"], 5 | "description": "A PHP Library to Convert HTML to AMP HTML", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Paul Redmond", 10 | "email": "paulrredmond@gmail.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { 15 | "Predmond\\HtmlToAmp\\": "src/" 16 | } 17 | }, 18 | "require": { 19 | "ext-xml": "*", 20 | "league/event": "^2.1" 21 | }, 22 | "require-dev": { 23 | "phpspec/phpspec": "^2.4", 24 | "phpspec/nyan-formatters": "^1.0" 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "0.1-dev" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "73eb73dfaf92465add1c70d52cb1bfa6", 8 | "content-hash": "f3f1cca2c8e0f45118dd2ca1f3961f56", 9 | "packages": [ 10 | { 11 | "name": "league/event", 12 | "version": "2.1.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/thephpleague/event.git", 16 | "reference": "e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/thephpleague/event/zipball/e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd", 21 | "reference": "e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": ">=5.4.0" 26 | }, 27 | "require-dev": { 28 | "henrikbjorn/phpspec-code-coverage": "~1.0.1", 29 | "phpspec/phpspec": "~2.0.0" 30 | }, 31 | "type": "library", 32 | "extra": { 33 | "branch-alias": { 34 | "dev-master": "2.2-dev" 35 | } 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "League\\Event\\": "src/" 40 | } 41 | }, 42 | "notification-url": "https://packagist.org/downloads/", 43 | "license": [ 44 | "MIT" 45 | ], 46 | "authors": [ 47 | { 48 | "name": "Frank de Jonge", 49 | "email": "info@frenky.net" 50 | } 51 | ], 52 | "description": "Event package", 53 | "keywords": [ 54 | "emitter", 55 | "event", 56 | "listener" 57 | ], 58 | "time": "2015-05-21 12:24:47" 59 | } 60 | ], 61 | "packages-dev": [ 62 | { 63 | "name": "doctrine/instantiator", 64 | "version": "1.0.5", 65 | "source": { 66 | "type": "git", 67 | "url": "https://github.com/doctrine/instantiator.git", 68 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 69 | }, 70 | "dist": { 71 | "type": "zip", 72 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 73 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 74 | "shasum": "" 75 | }, 76 | "require": { 77 | "php": ">=5.3,<8.0-DEV" 78 | }, 79 | "require-dev": { 80 | "athletic/athletic": "~0.1.8", 81 | "ext-pdo": "*", 82 | "ext-phar": "*", 83 | "phpunit/phpunit": "~4.0", 84 | "squizlabs/php_codesniffer": "~2.0" 85 | }, 86 | "type": "library", 87 | "extra": { 88 | "branch-alias": { 89 | "dev-master": "1.0.x-dev" 90 | } 91 | }, 92 | "autoload": { 93 | "psr-4": { 94 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 95 | } 96 | }, 97 | "notification-url": "https://packagist.org/downloads/", 98 | "license": [ 99 | "MIT" 100 | ], 101 | "authors": [ 102 | { 103 | "name": "Marco Pivetta", 104 | "email": "ocramius@gmail.com", 105 | "homepage": "http://ocramius.github.com/" 106 | } 107 | ], 108 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 109 | "homepage": "https://github.com/doctrine/instantiator", 110 | "keywords": [ 111 | "constructor", 112 | "instantiate" 113 | ], 114 | "time": "2015-06-14 21:17:01" 115 | }, 116 | { 117 | "name": "phpdocumentor/reflection-docblock", 118 | "version": "2.0.4", 119 | "source": { 120 | "type": "git", 121 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 122 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" 123 | }, 124 | "dist": { 125 | "type": "zip", 126 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", 127 | "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", 128 | "shasum": "" 129 | }, 130 | "require": { 131 | "php": ">=5.3.3" 132 | }, 133 | "require-dev": { 134 | "phpunit/phpunit": "~4.0" 135 | }, 136 | "suggest": { 137 | "dflydev/markdown": "~1.0", 138 | "erusev/parsedown": "~1.0" 139 | }, 140 | "type": "library", 141 | "extra": { 142 | "branch-alias": { 143 | "dev-master": "2.0.x-dev" 144 | } 145 | }, 146 | "autoload": { 147 | "psr-0": { 148 | "phpDocumentor": [ 149 | "src/" 150 | ] 151 | } 152 | }, 153 | "notification-url": "https://packagist.org/downloads/", 154 | "license": [ 155 | "MIT" 156 | ], 157 | "authors": [ 158 | { 159 | "name": "Mike van Riel", 160 | "email": "mike.vanriel@naenius.com" 161 | } 162 | ], 163 | "time": "2015-02-03 12:10:50" 164 | }, 165 | { 166 | "name": "phpspec/nyan-formatters", 167 | "version": "1.0.1", 168 | "source": { 169 | "type": "git", 170 | "url": "https://github.com/phpspec/nyan-formatters.git", 171 | "reference": "4aaef7454dc037208074798a34844e02300c33df" 172 | }, 173 | "dist": { 174 | "type": "zip", 175 | "url": "https://api.github.com/repos/phpspec/nyan-formatters/zipball/4aaef7454dc037208074798a34844e02300c33df", 176 | "reference": "4aaef7454dc037208074798a34844e02300c33df", 177 | "shasum": "" 178 | }, 179 | "require": { 180 | "php": ">=5.3.3", 181 | "phpspec/phpspec": "~2.0", 182 | "whatthejeff/nyancat-scoreboard": "~1.1" 183 | }, 184 | "type": "extension", 185 | "extra": { 186 | "branch-alias": { 187 | "dev-master": "1.0.x-dev" 188 | } 189 | }, 190 | "autoload": { 191 | "psr-0": { 192 | "PhpSpec\\NyanFormattersExtension": "src/" 193 | } 194 | }, 195 | "notification-url": "https://packagist.org/downloads/", 196 | "license": [ 197 | "MIT" 198 | ], 199 | "authors": [ 200 | { 201 | "name": "Jeff Welch", 202 | "email": "whatthejeff@gmail.com" 203 | }, 204 | { 205 | "name": "Matthew Davis", 206 | "email": "matt@mattdavis.co.uk" 207 | } 208 | ], 209 | "description": "PHPSpec Nyan Formatter Extension", 210 | "homepage": "https://github.com/phpspec/nyan-formatters", 211 | "keywords": [ 212 | "nyan", 213 | "phpspec" 214 | ], 215 | "time": "2014-07-16 13:27:03" 216 | }, 217 | { 218 | "name": "phpspec/php-diff", 219 | "version": "v1.0.2", 220 | "source": { 221 | "type": "git", 222 | "url": "https://github.com/phpspec/php-diff.git", 223 | "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" 224 | }, 225 | "dist": { 226 | "type": "zip", 227 | "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", 228 | "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", 229 | "shasum": "" 230 | }, 231 | "type": "library", 232 | "autoload": { 233 | "psr-0": { 234 | "Diff": "lib/" 235 | } 236 | }, 237 | "notification-url": "https://packagist.org/downloads/", 238 | "license": [ 239 | "BSD-3-Clause" 240 | ], 241 | "authors": [ 242 | { 243 | "name": "Chris Boulton", 244 | "homepage": "http://github.com/chrisboulton", 245 | "role": "Original developer" 246 | } 247 | ], 248 | "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", 249 | "time": "2013-11-01 13:02:21" 250 | }, 251 | { 252 | "name": "phpspec/phpspec", 253 | "version": "2.5.0", 254 | "source": { 255 | "type": "git", 256 | "url": "https://github.com/phpspec/phpspec.git", 257 | "reference": "385ecb015e97c13818074f1517928b24d4a26067" 258 | }, 259 | "dist": { 260 | "type": "zip", 261 | "url": "https://api.github.com/repos/phpspec/phpspec/zipball/385ecb015e97c13818074f1517928b24d4a26067", 262 | "reference": "385ecb015e97c13818074f1517928b24d4a26067", 263 | "shasum": "" 264 | }, 265 | "require": { 266 | "doctrine/instantiator": "^1.0.1", 267 | "ext-tokenizer": "*", 268 | "php": ">=5.3.3", 269 | "phpspec/php-diff": "~1.0.0", 270 | "phpspec/prophecy": "~1.4", 271 | "sebastian/exporter": "~1.0", 272 | "symfony/console": "~2.3|~3.0", 273 | "symfony/event-dispatcher": "~2.1|~3.0", 274 | "symfony/finder": "~2.1|~3.0", 275 | "symfony/process": "^2.6|~3.0", 276 | "symfony/yaml": "~2.1|~3.0" 277 | }, 278 | "require-dev": { 279 | "behat/behat": "^3.0.11", 280 | "bossa/phpspec2-expect": "~1.0", 281 | "phpunit/phpunit": "~4.4", 282 | "symfony/filesystem": "~2.1|~3.0" 283 | }, 284 | "suggest": { 285 | "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" 286 | }, 287 | "bin": [ 288 | "bin/phpspec" 289 | ], 290 | "type": "library", 291 | "extra": { 292 | "branch-alias": { 293 | "dev-master": "2.2.x-dev" 294 | } 295 | }, 296 | "autoload": { 297 | "psr-0": { 298 | "PhpSpec": "src/" 299 | } 300 | }, 301 | "notification-url": "https://packagist.org/downloads/", 302 | "license": [ 303 | "MIT" 304 | ], 305 | "authors": [ 306 | { 307 | "name": "Konstantin Kudryashov", 308 | "email": "ever.zet@gmail.com", 309 | "homepage": "http://everzet.com" 310 | }, 311 | { 312 | "name": "Marcello Duarte", 313 | "homepage": "http://marcelloduarte.net/" 314 | } 315 | ], 316 | "description": "Specification-oriented BDD framework for PHP 5.3+", 317 | "homepage": "http://phpspec.net/", 318 | "keywords": [ 319 | "BDD", 320 | "SpecBDD", 321 | "TDD", 322 | "spec", 323 | "specification", 324 | "testing", 325 | "tests" 326 | ], 327 | "time": "2016-03-20 20:34:32" 328 | }, 329 | { 330 | "name": "phpspec/prophecy", 331 | "version": "v1.6.0", 332 | "source": { 333 | "type": "git", 334 | "url": "https://github.com/phpspec/prophecy.git", 335 | "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972" 336 | }, 337 | "dist": { 338 | "type": "zip", 339 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3c91bdf81797d725b14cb62906f9a4ce44235972", 340 | "reference": "3c91bdf81797d725b14cb62906f9a4ce44235972", 341 | "shasum": "" 342 | }, 343 | "require": { 344 | "doctrine/instantiator": "^1.0.2", 345 | "php": "^5.3|^7.0", 346 | "phpdocumentor/reflection-docblock": "~2.0", 347 | "sebastian/comparator": "~1.1", 348 | "sebastian/recursion-context": "~1.0" 349 | }, 350 | "require-dev": { 351 | "phpspec/phpspec": "~2.0" 352 | }, 353 | "type": "library", 354 | "extra": { 355 | "branch-alias": { 356 | "dev-master": "1.5.x-dev" 357 | } 358 | }, 359 | "autoload": { 360 | "psr-0": { 361 | "Prophecy\\": "src/" 362 | } 363 | }, 364 | "notification-url": "https://packagist.org/downloads/", 365 | "license": [ 366 | "MIT" 367 | ], 368 | "authors": [ 369 | { 370 | "name": "Konstantin Kudryashov", 371 | "email": "ever.zet@gmail.com", 372 | "homepage": "http://everzet.com" 373 | }, 374 | { 375 | "name": "Marcello Duarte", 376 | "email": "marcello.duarte@gmail.com" 377 | } 378 | ], 379 | "description": "Highly opinionated mocking framework for PHP 5.3+", 380 | "homepage": "https://github.com/phpspec/prophecy", 381 | "keywords": [ 382 | "Double", 383 | "Dummy", 384 | "fake", 385 | "mock", 386 | "spy", 387 | "stub" 388 | ], 389 | "time": "2016-02-15 07:46:21" 390 | }, 391 | { 392 | "name": "sebastian/comparator", 393 | "version": "1.2.0", 394 | "source": { 395 | "type": "git", 396 | "url": "https://github.com/sebastianbergmann/comparator.git", 397 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" 398 | }, 399 | "dist": { 400 | "type": "zip", 401 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", 402 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", 403 | "shasum": "" 404 | }, 405 | "require": { 406 | "php": ">=5.3.3", 407 | "sebastian/diff": "~1.2", 408 | "sebastian/exporter": "~1.2" 409 | }, 410 | "require-dev": { 411 | "phpunit/phpunit": "~4.4" 412 | }, 413 | "type": "library", 414 | "extra": { 415 | "branch-alias": { 416 | "dev-master": "1.2.x-dev" 417 | } 418 | }, 419 | "autoload": { 420 | "classmap": [ 421 | "src/" 422 | ] 423 | }, 424 | "notification-url": "https://packagist.org/downloads/", 425 | "license": [ 426 | "BSD-3-Clause" 427 | ], 428 | "authors": [ 429 | { 430 | "name": "Jeff Welch", 431 | "email": "whatthejeff@gmail.com" 432 | }, 433 | { 434 | "name": "Volker Dusch", 435 | "email": "github@wallbash.com" 436 | }, 437 | { 438 | "name": "Bernhard Schussek", 439 | "email": "bschussek@2bepublished.at" 440 | }, 441 | { 442 | "name": "Sebastian Bergmann", 443 | "email": "sebastian@phpunit.de" 444 | } 445 | ], 446 | "description": "Provides the functionality to compare PHP values for equality", 447 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 448 | "keywords": [ 449 | "comparator", 450 | "compare", 451 | "equality" 452 | ], 453 | "time": "2015-07-26 15:48:44" 454 | }, 455 | { 456 | "name": "sebastian/diff", 457 | "version": "1.4.1", 458 | "source": { 459 | "type": "git", 460 | "url": "https://github.com/sebastianbergmann/diff.git", 461 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 462 | }, 463 | "dist": { 464 | "type": "zip", 465 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 466 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 467 | "shasum": "" 468 | }, 469 | "require": { 470 | "php": ">=5.3.3" 471 | }, 472 | "require-dev": { 473 | "phpunit/phpunit": "~4.8" 474 | }, 475 | "type": "library", 476 | "extra": { 477 | "branch-alias": { 478 | "dev-master": "1.4-dev" 479 | } 480 | }, 481 | "autoload": { 482 | "classmap": [ 483 | "src/" 484 | ] 485 | }, 486 | "notification-url": "https://packagist.org/downloads/", 487 | "license": [ 488 | "BSD-3-Clause" 489 | ], 490 | "authors": [ 491 | { 492 | "name": "Kore Nordmann", 493 | "email": "mail@kore-nordmann.de" 494 | }, 495 | { 496 | "name": "Sebastian Bergmann", 497 | "email": "sebastian@phpunit.de" 498 | } 499 | ], 500 | "description": "Diff implementation", 501 | "homepage": "https://github.com/sebastianbergmann/diff", 502 | "keywords": [ 503 | "diff" 504 | ], 505 | "time": "2015-12-08 07:14:41" 506 | }, 507 | { 508 | "name": "sebastian/exporter", 509 | "version": "1.2.1", 510 | "source": { 511 | "type": "git", 512 | "url": "https://github.com/sebastianbergmann/exporter.git", 513 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e" 514 | }, 515 | "dist": { 516 | "type": "zip", 517 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", 518 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e", 519 | "shasum": "" 520 | }, 521 | "require": { 522 | "php": ">=5.3.3", 523 | "sebastian/recursion-context": "~1.0" 524 | }, 525 | "require-dev": { 526 | "phpunit/phpunit": "~4.4" 527 | }, 528 | "type": "library", 529 | "extra": { 530 | "branch-alias": { 531 | "dev-master": "1.2.x-dev" 532 | } 533 | }, 534 | "autoload": { 535 | "classmap": [ 536 | "src/" 537 | ] 538 | }, 539 | "notification-url": "https://packagist.org/downloads/", 540 | "license": [ 541 | "BSD-3-Clause" 542 | ], 543 | "authors": [ 544 | { 545 | "name": "Jeff Welch", 546 | "email": "whatthejeff@gmail.com" 547 | }, 548 | { 549 | "name": "Volker Dusch", 550 | "email": "github@wallbash.com" 551 | }, 552 | { 553 | "name": "Bernhard Schussek", 554 | "email": "bschussek@2bepublished.at" 555 | }, 556 | { 557 | "name": "Sebastian Bergmann", 558 | "email": "sebastian@phpunit.de" 559 | }, 560 | { 561 | "name": "Adam Harvey", 562 | "email": "aharvey@php.net" 563 | } 564 | ], 565 | "description": "Provides the functionality to export PHP variables for visualization", 566 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 567 | "keywords": [ 568 | "export", 569 | "exporter" 570 | ], 571 | "time": "2015-06-21 07:55:53" 572 | }, 573 | { 574 | "name": "sebastian/recursion-context", 575 | "version": "1.0.2", 576 | "source": { 577 | "type": "git", 578 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 579 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791" 580 | }, 581 | "dist": { 582 | "type": "zip", 583 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", 584 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791", 585 | "shasum": "" 586 | }, 587 | "require": { 588 | "php": ">=5.3.3" 589 | }, 590 | "require-dev": { 591 | "phpunit/phpunit": "~4.4" 592 | }, 593 | "type": "library", 594 | "extra": { 595 | "branch-alias": { 596 | "dev-master": "1.0.x-dev" 597 | } 598 | }, 599 | "autoload": { 600 | "classmap": [ 601 | "src/" 602 | ] 603 | }, 604 | "notification-url": "https://packagist.org/downloads/", 605 | "license": [ 606 | "BSD-3-Clause" 607 | ], 608 | "authors": [ 609 | { 610 | "name": "Jeff Welch", 611 | "email": "whatthejeff@gmail.com" 612 | }, 613 | { 614 | "name": "Sebastian Bergmann", 615 | "email": "sebastian@phpunit.de" 616 | }, 617 | { 618 | "name": "Adam Harvey", 619 | "email": "aharvey@php.net" 620 | } 621 | ], 622 | "description": "Provides functionality to recursively process PHP variables", 623 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 624 | "time": "2015-11-11 19:50:13" 625 | }, 626 | { 627 | "name": "symfony/console", 628 | "version": "v3.0.4", 629 | "source": { 630 | "type": "git", 631 | "url": "https://github.com/symfony/console.git", 632 | "reference": "6b1175135bc2a74c08a28d89761272de8beed8cd" 633 | }, 634 | "dist": { 635 | "type": "zip", 636 | "url": "https://api.github.com/repos/symfony/console/zipball/6b1175135bc2a74c08a28d89761272de8beed8cd", 637 | "reference": "6b1175135bc2a74c08a28d89761272de8beed8cd", 638 | "shasum": "" 639 | }, 640 | "require": { 641 | "php": ">=5.5.9", 642 | "symfony/polyfill-mbstring": "~1.0" 643 | }, 644 | "require-dev": { 645 | "psr/log": "~1.0", 646 | "symfony/event-dispatcher": "~2.8|~3.0", 647 | "symfony/process": "~2.8|~3.0" 648 | }, 649 | "suggest": { 650 | "psr/log": "For using the console logger", 651 | "symfony/event-dispatcher": "", 652 | "symfony/process": "" 653 | }, 654 | "type": "library", 655 | "extra": { 656 | "branch-alias": { 657 | "dev-master": "3.0-dev" 658 | } 659 | }, 660 | "autoload": { 661 | "psr-4": { 662 | "Symfony\\Component\\Console\\": "" 663 | }, 664 | "exclude-from-classmap": [ 665 | "/Tests/" 666 | ] 667 | }, 668 | "notification-url": "https://packagist.org/downloads/", 669 | "license": [ 670 | "MIT" 671 | ], 672 | "authors": [ 673 | { 674 | "name": "Fabien Potencier", 675 | "email": "fabien@symfony.com" 676 | }, 677 | { 678 | "name": "Symfony Community", 679 | "homepage": "https://symfony.com/contributors" 680 | } 681 | ], 682 | "description": "Symfony Console Component", 683 | "homepage": "https://symfony.com", 684 | "time": "2016-03-16 17:00:50" 685 | }, 686 | { 687 | "name": "symfony/event-dispatcher", 688 | "version": "v3.0.4", 689 | "source": { 690 | "type": "git", 691 | "url": "https://github.com/symfony/event-dispatcher.git", 692 | "reference": "9002dcf018d884d294b1ef20a6f968efc1128f39" 693 | }, 694 | "dist": { 695 | "type": "zip", 696 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9002dcf018d884d294b1ef20a6f968efc1128f39", 697 | "reference": "9002dcf018d884d294b1ef20a6f968efc1128f39", 698 | "shasum": "" 699 | }, 700 | "require": { 701 | "php": ">=5.5.9" 702 | }, 703 | "require-dev": { 704 | "psr/log": "~1.0", 705 | "symfony/config": "~2.8|~3.0", 706 | "symfony/dependency-injection": "~2.8|~3.0", 707 | "symfony/expression-language": "~2.8|~3.0", 708 | "symfony/stopwatch": "~2.8|~3.0" 709 | }, 710 | "suggest": { 711 | "symfony/dependency-injection": "", 712 | "symfony/http-kernel": "" 713 | }, 714 | "type": "library", 715 | "extra": { 716 | "branch-alias": { 717 | "dev-master": "3.0-dev" 718 | } 719 | }, 720 | "autoload": { 721 | "psr-4": { 722 | "Symfony\\Component\\EventDispatcher\\": "" 723 | }, 724 | "exclude-from-classmap": [ 725 | "/Tests/" 726 | ] 727 | }, 728 | "notification-url": "https://packagist.org/downloads/", 729 | "license": [ 730 | "MIT" 731 | ], 732 | "authors": [ 733 | { 734 | "name": "Fabien Potencier", 735 | "email": "fabien@symfony.com" 736 | }, 737 | { 738 | "name": "Symfony Community", 739 | "homepage": "https://symfony.com/contributors" 740 | } 741 | ], 742 | "description": "Symfony EventDispatcher Component", 743 | "homepage": "https://symfony.com", 744 | "time": "2016-03-10 10:34:12" 745 | }, 746 | { 747 | "name": "symfony/finder", 748 | "version": "v3.0.4", 749 | "source": { 750 | "type": "git", 751 | "url": "https://github.com/symfony/finder.git", 752 | "reference": "c54e407b35bc098916704e9fd090da21da4c4f52" 753 | }, 754 | "dist": { 755 | "type": "zip", 756 | "url": "https://api.github.com/repos/symfony/finder/zipball/c54e407b35bc098916704e9fd090da21da4c4f52", 757 | "reference": "c54e407b35bc098916704e9fd090da21da4c4f52", 758 | "shasum": "" 759 | }, 760 | "require": { 761 | "php": ">=5.5.9" 762 | }, 763 | "type": "library", 764 | "extra": { 765 | "branch-alias": { 766 | "dev-master": "3.0-dev" 767 | } 768 | }, 769 | "autoload": { 770 | "psr-4": { 771 | "Symfony\\Component\\Finder\\": "" 772 | }, 773 | "exclude-from-classmap": [ 774 | "/Tests/" 775 | ] 776 | }, 777 | "notification-url": "https://packagist.org/downloads/", 778 | "license": [ 779 | "MIT" 780 | ], 781 | "authors": [ 782 | { 783 | "name": "Fabien Potencier", 784 | "email": "fabien@symfony.com" 785 | }, 786 | { 787 | "name": "Symfony Community", 788 | "homepage": "https://symfony.com/contributors" 789 | } 790 | ], 791 | "description": "Symfony Finder Component", 792 | "homepage": "https://symfony.com", 793 | "time": "2016-03-10 11:13:05" 794 | }, 795 | { 796 | "name": "symfony/polyfill-mbstring", 797 | "version": "v1.1.1", 798 | "source": { 799 | "type": "git", 800 | "url": "https://github.com/symfony/polyfill-mbstring.git", 801 | "reference": "1289d16209491b584839022f29257ad859b8532d" 802 | }, 803 | "dist": { 804 | "type": "zip", 805 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/1289d16209491b584839022f29257ad859b8532d", 806 | "reference": "1289d16209491b584839022f29257ad859b8532d", 807 | "shasum": "" 808 | }, 809 | "require": { 810 | "php": ">=5.3.3" 811 | }, 812 | "suggest": { 813 | "ext-mbstring": "For best performance" 814 | }, 815 | "type": "library", 816 | "extra": { 817 | "branch-alias": { 818 | "dev-master": "1.1-dev" 819 | } 820 | }, 821 | "autoload": { 822 | "psr-4": { 823 | "Symfony\\Polyfill\\Mbstring\\": "" 824 | }, 825 | "files": [ 826 | "bootstrap.php" 827 | ] 828 | }, 829 | "notification-url": "https://packagist.org/downloads/", 830 | "license": [ 831 | "MIT" 832 | ], 833 | "authors": [ 834 | { 835 | "name": "Nicolas Grekas", 836 | "email": "p@tchwork.com" 837 | }, 838 | { 839 | "name": "Symfony Community", 840 | "homepage": "https://symfony.com/contributors" 841 | } 842 | ], 843 | "description": "Symfony polyfill for the Mbstring extension", 844 | "homepage": "https://symfony.com", 845 | "keywords": [ 846 | "compatibility", 847 | "mbstring", 848 | "polyfill", 849 | "portable", 850 | "shim" 851 | ], 852 | "time": "2016-01-20 09:13:37" 853 | }, 854 | { 855 | "name": "symfony/process", 856 | "version": "v3.0.4", 857 | "source": { 858 | "type": "git", 859 | "url": "https://github.com/symfony/process.git", 860 | "reference": "e6f1f98bbd355d209a992bfff45e7edfbd4a0776" 861 | }, 862 | "dist": { 863 | "type": "zip", 864 | "url": "https://api.github.com/repos/symfony/process/zipball/e6f1f98bbd355d209a992bfff45e7edfbd4a0776", 865 | "reference": "e6f1f98bbd355d209a992bfff45e7edfbd4a0776", 866 | "shasum": "" 867 | }, 868 | "require": { 869 | "php": ">=5.5.9" 870 | }, 871 | "type": "library", 872 | "extra": { 873 | "branch-alias": { 874 | "dev-master": "3.0-dev" 875 | } 876 | }, 877 | "autoload": { 878 | "psr-4": { 879 | "Symfony\\Component\\Process\\": "" 880 | }, 881 | "exclude-from-classmap": [ 882 | "/Tests/" 883 | ] 884 | }, 885 | "notification-url": "https://packagist.org/downloads/", 886 | "license": [ 887 | "MIT" 888 | ], 889 | "authors": [ 890 | { 891 | "name": "Fabien Potencier", 892 | "email": "fabien@symfony.com" 893 | }, 894 | { 895 | "name": "Symfony Community", 896 | "homepage": "https://symfony.com/contributors" 897 | } 898 | ], 899 | "description": "Symfony Process Component", 900 | "homepage": "https://symfony.com", 901 | "time": "2016-03-30 10:41:14" 902 | }, 903 | { 904 | "name": "symfony/yaml", 905 | "version": "v3.0.4", 906 | "source": { 907 | "type": "git", 908 | "url": "https://github.com/symfony/yaml.git", 909 | "reference": "0047c8366744a16de7516622c5b7355336afae96" 910 | }, 911 | "dist": { 912 | "type": "zip", 913 | "url": "https://api.github.com/repos/symfony/yaml/zipball/0047c8366744a16de7516622c5b7355336afae96", 914 | "reference": "0047c8366744a16de7516622c5b7355336afae96", 915 | "shasum": "" 916 | }, 917 | "require": { 918 | "php": ">=5.5.9" 919 | }, 920 | "type": "library", 921 | "extra": { 922 | "branch-alias": { 923 | "dev-master": "3.0-dev" 924 | } 925 | }, 926 | "autoload": { 927 | "psr-4": { 928 | "Symfony\\Component\\Yaml\\": "" 929 | }, 930 | "exclude-from-classmap": [ 931 | "/Tests/" 932 | ] 933 | }, 934 | "notification-url": "https://packagist.org/downloads/", 935 | "license": [ 936 | "MIT" 937 | ], 938 | "authors": [ 939 | { 940 | "name": "Fabien Potencier", 941 | "email": "fabien@symfony.com" 942 | }, 943 | { 944 | "name": "Symfony Community", 945 | "homepage": "https://symfony.com/contributors" 946 | } 947 | ], 948 | "description": "Symfony Yaml Component", 949 | "homepage": "https://symfony.com", 950 | "time": "2016-03-04 07:55:57" 951 | }, 952 | { 953 | "name": "whatthejeff/fab", 954 | "version": "v1.0.0", 955 | "source": { 956 | "type": "git", 957 | "url": "https://github.com/whatthejeff/fab.git", 958 | "reference": "1f9d9cdc354cabda0d7b72b7e2ab5fdfb747b8ef" 959 | }, 960 | "dist": { 961 | "type": "zip", 962 | "url": "https://api.github.com/repos/whatthejeff/fab/zipball/1f9d9cdc354cabda0d7b72b7e2ab5fdfb747b8ef", 963 | "reference": "1f9d9cdc354cabda0d7b72b7e2ab5fdfb747b8ef", 964 | "shasum": "" 965 | }, 966 | "require": { 967 | "php": ">=5.3.3" 968 | }, 969 | "require-dev": { 970 | "phpunit/phpunit": "3.7.*" 971 | }, 972 | "type": "library", 973 | "extra": { 974 | "branch-alias": { 975 | "dev-master": "1.0.x-dev" 976 | } 977 | }, 978 | "autoload": { 979 | "psr-0": { 980 | "Fab": "src/" 981 | } 982 | }, 983 | "notification-url": "https://packagist.org/downloads/", 984 | "license": [ 985 | "MIT" 986 | ], 987 | "authors": [ 988 | { 989 | "name": "Jeff Welch", 990 | "email": "whatthejeff@gmail.com" 991 | } 992 | ], 993 | "description": "Make your output fabulous!", 994 | "homepage": "http://github.com/whatthejeff/fab", 995 | "keywords": [ 996 | "colorful", 997 | "fab", 998 | "fabulous", 999 | "rainbow" 1000 | ], 1001 | "time": "2013-02-14 01:28:47" 1002 | }, 1003 | { 1004 | "name": "whatthejeff/nyancat-scoreboard", 1005 | "version": "v1.1.1", 1006 | "source": { 1007 | "type": "git", 1008 | "url": "https://github.com/whatthejeff/nyancat-scoreboard.git", 1009 | "reference": "ab5e68605d4950f299684f4e161d56e96be6994c" 1010 | }, 1011 | "dist": { 1012 | "type": "zip", 1013 | "url": "https://api.github.com/repos/whatthejeff/nyancat-scoreboard/zipball/ab5e68605d4950f299684f4e161d56e96be6994c", 1014 | "reference": "ab5e68605d4950f299684f4e161d56e96be6994c", 1015 | "shasum": "" 1016 | }, 1017 | "require": { 1018 | "php": ">=5.3.3", 1019 | "whatthejeff/fab": "~1.0" 1020 | }, 1021 | "require-dev": { 1022 | "pear-pear/pear": "1.9.4", 1023 | "phpunit/php-code-coverage": "1.3.*@dev", 1024 | "phpunit/phpunit": "3.8.*@dev", 1025 | "phpunit/phpunit-mock-objects": "1.3.*@dev" 1026 | }, 1027 | "type": "library", 1028 | "extra": { 1029 | "branch-alias": { 1030 | "dev-master": "1.1.x-dev" 1031 | } 1032 | }, 1033 | "autoload": { 1034 | "psr-0": { 1035 | "NyanCat": "src/" 1036 | } 1037 | }, 1038 | "notification-url": "https://packagist.org/downloads/", 1039 | "license": [ 1040 | "MIT" 1041 | ], 1042 | "authors": [ 1043 | { 1044 | "name": "Jeff Welch", 1045 | "email": "whatthejeff@gmail.com" 1046 | } 1047 | ], 1048 | "description": "Nyan Cat Scoreboard", 1049 | "homepage": "http://github.com/whatthejeff/nyancat-scoreboard", 1050 | "keywords": [ 1051 | "cat", 1052 | "nyan", 1053 | "rainbow", 1054 | "scoreboard", 1055 | "tests" 1056 | ], 1057 | "time": "2014-02-12 22:16:49" 1058 | } 1059 | ], 1060 | "aliases": [], 1061 | "minimum-stability": "stable", 1062 | "stability-flags": [], 1063 | "prefer-stable": false, 1064 | "prefer-lowest": false, 1065 | "platform": { 1066 | "ext-xml": "*" 1067 | }, 1068 | "platform-dev": [] 1069 | } 1070 | -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | default: 3 | namespace: Predmond\HtmlToAmp 4 | psr4_prefix: Predmond\HtmlToAmp 5 | extensions: 6 | - PhpSpec\NyanFormattersExtension\Extension 7 | formatter.name: nyan.cat -------------------------------------------------------------------------------- /spec/AmpConverterSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Predmond\HtmlToAmp\AmpConverter'); 13 | } 14 | 15 | public function it_converts_spaces_to_an_empty_string() 16 | { 17 | $this->convert(' ')->shouldReturn(''); 18 | } 19 | 20 | function it_converts_html_to_amp() 21 | { 22 | $this->convert(implode('', [ 23 | '

Lorem ipsum dolor sit amet, consectetur adipisicing elit.

', 24 | '', 25 | '

Aut blanditiis exercitationem in, incidunt odit optio.

' 26 | ]))->shouldReturn(implode('', [ 27 | '

Lorem ipsum dolor sit amet, consectetur adipisicing elit.

', 28 | '', 29 | '

Aut blanditiis exercitationem in, incidunt odit optio.

' 30 | ])); 31 | } 32 | 33 | function it_should_remove_prohibited_tags() 34 | { 35 | $convert = [ 36 | '', 37 | '', 38 | '', 39 | '
', 40 | '', 41 | '', 42 | '', 43 | '
', 44 | '

Hello World Example

', 45 | 'Alert Foo', 46 | '', 47 | '', 48 | '', 49 | '', 50 | '', 51 | ]; 52 | 53 | $this 54 | ->convert(implode("\n", $convert)) 55 | ->shouldReturn(implode("\n", [ 56 | '', 57 | '

Hello World Example

' 58 | ])); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /spec/Converter/ConverterInterface.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulredmond/html-to-amp/7062ce7d78ea066c87cc52fecd6710e0082bf80c/spec/Converter/ConverterInterface.php -------------------------------------------------------------------------------- /spec/Converter/Extensions/InstagramConverterSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Predmond\HtmlToAmp\Converter\Extensions\InstagramConverter'); 18 | $this->shouldHaveType('Predmond\HtmlToAmp\Converter\ConverterInterface'); 19 | } 20 | 21 | public function it_should_be_converted(ElementInterface $element) 22 | { 23 | $element->hasChildren()->willReturn(true); 24 | $element->getAttribute('class')->willReturn('instagram-media or more'); 25 | $element->getAttributes()->willReturn(['data-instgrm-version']); 26 | $this->convertToAmp($element)->shouldReturn(true); 27 | } 28 | 29 | public function it_should_not_be_converted(ElementInterface $element) 30 | { 31 | $element->hasChildren()->willReturn(false); 32 | $element->getAttribute('class')->willReturn('some class'); 33 | $this->convertToAmp($element)->shouldReturn(false); 34 | } 35 | 36 | public function it_finds_the_instagram_code(ElementInterface $element) 37 | { 38 | $prophet = new Prophet(); 39 | 40 | $anchor = $prophet->prophesize('Predmond\HtmlToAmp\Element'); 41 | $anchor->getAttribute('href')->willReturn('https://www.instagram.com/p/AAA-aaa/'); 42 | 43 | $sibling = $prophet->prophesize('Predmond\HtmlToAmp\Element'); 44 | $sibling->getAttribute('href')->willReturn(''); 45 | 46 | $parent = $prophet->prophesize('Predmond\HtmlToAmp\Element'); 47 | $parent->getAttribute('href')->willReturn(''); 48 | $parent->getChildren()->willReturn([ $anchor, $sibling ]); 49 | 50 | $element->getAttribute('href')->willReturn(''); 51 | $element->getChildren()->willReturn([ $parent]); 52 | 53 | $this->getEmbedShortcode($element)->shouldBe('AAA-aaa'); 54 | } 55 | 56 | public function it_replaces_element_class_instagram_with_amp( 57 | EventInterface $event, 58 | ElementInterface $element 59 | ) { 60 | $this->handleInstagram($event, $element); 61 | } 62 | 63 | public function it_has_subscribed_events() 64 | { 65 | $this->getSubscribedEvents()->shouldReturn([ 66 | 'blockquote' => ['handleInstagram'] 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /spec/Converter/Extensions/YoutubeConverterSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(YoutubeConverter::class); 17 | } 18 | 19 | function it_can_convert_a_youtube_iframe_tag( 20 | EventInterface $event, 21 | ElementInterface $element, 22 | Element $writeableElement, 23 | Element $ampYoutubeElement 24 | ) { 25 | $event 26 | ->stopPropagation() 27 | ->shouldBeCalled() 28 | ; 29 | 30 | $writeableElement 31 | ->appendChild($ampYoutubeElement) 32 | ->shouldBeCalled() 33 | ; 34 | 35 | $element 36 | ->getAttribute('src') 37 | ->shouldBeCalled() 38 | ->willReturn('http://www.youtube.com/embed/XGSy3_Czz8k?autoplay=1') 39 | ; 40 | 41 | $element 42 | ->createWritableElement('div', [ 43 | 'class' => 'youtube-container' 44 | ]) 45 | ->shouldBeCalled() 46 | ->willReturn($writeableElement) 47 | ; 48 | 49 | $element->replaceWith($writeableElement)->shouldBeCalled(); 50 | 51 | $element 52 | ->createWritableElement( 53 | 'amp-youtube', 54 | [ 55 | 'data-videoid' => 'XGSy3_Czz8k', 56 | "layout" => "responsive", 57 | "width" => "560", 58 | "height" => "315" 59 | ] 60 | ) 61 | ->shouldBeCalled() 62 | ->willReturn($ampYoutubeElement) 63 | ; 64 | 65 | $this->handleIframe($event, $element); 66 | } 67 | 68 | function it_skips_converting_non_youtube_iframes( 69 | EventInterface $event, 70 | ElementInterface $element 71 | ) { 72 | $element 73 | ->getAttribute('src') 74 | ->shouldBeCalled() 75 | ->willReturn('http://metube.com/embed/XGSy3_Czz8k') 76 | ; 77 | 78 | $event->stopPropagation()->shouldNotBeCalled(); 79 | $element->createWritableElement('div', [ 80 | 'class' => 'youtube-container' 81 | ])->shouldNotBeCalled(); 82 | 83 | $this->handleIframe($event, $element); 84 | } 85 | 86 | function it_converts_a_youtube_object_tag( 87 | EventInterface $event, 88 | ElementInterface $element, 89 | Element $child, 90 | Element $containerElement, 91 | Element $ampYoutubeElement 92 | ) { 93 | $event 94 | ->stopPropagation() 95 | ->shouldBeCalled() 96 | ; 97 | 98 | $element 99 | ->getChildren() 100 | ->shouldBeCalled() 101 | ->willReturn([$child]) 102 | ; 103 | 104 | $child 105 | ->getAttribute('value') 106 | ->shouldBeCalled() 107 | ->willReturn('http://www.youtube.com/embed/XGSy3_Czz8k?autoplay=1') 108 | ; 109 | 110 | $element 111 | ->createWritableElement('div', [ 112 | 'class' => 'youtube-container' 113 | ]) 114 | ->shouldBeCalled() 115 | ->willReturn($containerElement) 116 | ; 117 | 118 | $containerElement 119 | ->appendChild($ampYoutubeElement) 120 | ->shouldBeCalled() 121 | ; 122 | 123 | $element 124 | ->replaceWith($containerElement) 125 | ->shouldBeCalled() 126 | ; 127 | 128 | $element 129 | ->createWritableElement( 130 | 'amp-youtube', 131 | [ 132 | 'data-videoid' => 'XGSy3_Czz8k', 133 | 'layout' => 'responsive', 134 | 'width' => '560', 135 | 'height' => '315' 136 | ] 137 | ) 138 | ->shouldBeCalled() 139 | ->willReturn($ampYoutubeElement) 140 | ; 141 | 142 | $this->handleObject($event, $element); 143 | } 144 | 145 | function it_skips_converting_non_youtube_object_tags( 146 | EventInterface $event, 147 | ElementInterface $element, 148 | ElementInterface $childElement 149 | ) { 150 | $element 151 | ->getChildren() 152 | ->shouldBeCalled() 153 | ->willReturn([$childElement]) 154 | ; 155 | 156 | $childElement 157 | ->getAttribute('value') 158 | ->shouldBeCalled() 159 | ->willReturn('http://metube.com/embed/XGSy3_Czz8k') 160 | ; 161 | 162 | $event->stopPropagation()->shouldNotBeCalled(); 163 | $element->createWritableElement('div', [ 164 | 'class' => 'youtube-container' 165 | ])->shouldNotBeCalled(); 166 | 167 | $this->handleObject($event, $element); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /spec/Converter/ImageConverterSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Predmond\HtmlToAmp\Converter\ImageConverter'); 16 | $this->shouldHaveType('Predmond\HtmlToAmp\Converter\ConverterInterface'); 17 | } 18 | 19 | public function it_converts_an_image_to_amp_img( 20 | ElementInterface $ampImg, 21 | EventInterface $event, 22 | ElementInterface $element 23 | ) { 24 | $ampImg->setAttribute('src', 'foo.jpg')->shouldBeCalled(); 25 | $ampImg->setAttribute('width', 300)->shouldBeCalled(); 26 | $ampImg->setAttribute('height', 250)->shouldBeCalled(); 27 | $ampImg->setAttribute('class', 'amp-img')->shouldBeCalled(); 28 | $ampImg->setAttribute('srcset', '')->shouldNotBeCalled(); 29 | $ampImg->setAttribute('alt', '')->shouldNotBeCalled(); 30 | $ampImg->setAttribute('attribution', '')->shouldNotBeCalled(); 31 | $ampImg->setAttribute('layout', '')->shouldNotBeCalled(); 32 | 33 | $element->getAttribute('src')->shouldBeCalled()->willReturn('foo.jpg'); 34 | $element->getAttribute('width')->shouldBeCalled()->willReturn(300); 35 | $element->getAttribute('height')->shouldBeCalled()->willReturn(250); 36 | $element->getAttribute('class')->shouldBeCalled()->willReturn('amp-img'); 37 | $element->getAttribute('srcset')->shouldBeCalled()->willReturn(''); 38 | $element->getAttribute('alt')->shouldBeCalled()->willReturn(''); 39 | $element->getAttribute('attribution')->shouldBeCalled()->willReturn(''); 40 | $element->getAttribute('layout')->shouldBeCalled()->willReturn(''); 41 | 42 | $element 43 | ->createWritableElement('amp-img') 44 | ->willReturn($ampImg); 45 | 46 | $element->replaceWith($ampImg)->shouldBeCalled(); 47 | 48 | $this->handleTagImg($event, $element); 49 | } 50 | 51 | /** @test **/ 52 | public function it_has_subscribed_events() 53 | { 54 | $this->getSubscribedEvents()->shouldReturn([ 55 | 'img' => ['handleTagImg'] 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /spec/ElementSpec.php: -------------------------------------------------------------------------------- 1 | createElement('div', 'Hello world!'); 17 | 18 | $node->setAttribute('id', 'welcome'); 19 | $node->setAttribute('class', 'welcome big'); 20 | 21 | $this->beConstructedWith($node); 22 | } 23 | 24 | function it_is_initializable() 25 | { 26 | $this->shouldHaveType('Predmond\HtmlToAmp\Element'); 27 | $this->shouldHaveType('Predmond\HtmlToAmp\ElementInterface'); 28 | } 29 | 30 | function it_responds_to_getNode() 31 | { 32 | $this->getNode()->shouldReturnAnInstanceOf('DOMElement'); 33 | } 34 | 35 | function it_can_get_the_tag_name() 36 | { 37 | $this->getTagName()->shouldReturn("div"); 38 | } 39 | 40 | public function it_responds_to_getValue() 41 | { 42 | $this->getValue()->shouldReturn('Hello world!'); 43 | } 44 | 45 | /** @test **/ 46 | public function it_can_get_an_attribute() 47 | { 48 | $this->getAttribute('id')->shouldReturn('welcome'); 49 | $this->getAttribute('foo')->shouldReturn(''); 50 | } 51 | 52 | public function it_can_set_an_attribute() 53 | { 54 | $this->setAttribute('foo', 'bar')->shouldReturn(null); 55 | $this->getAttribute('foo')->shouldReturn('bar'); 56 | } 57 | 58 | /** @test **/ 59 | public function it_gets_an_attribute_of_a_non_DOMElement() 60 | { 61 | $node = (new DOMDocument('1.0', 'utf-8')) 62 | ->createTextNode('Hello World'); 63 | 64 | $this->beConstructedWith($node); 65 | 66 | $this->getAttribute('foo')->shouldReturn(''); 67 | } 68 | 69 | /** @test **/ 70 | public function it_can_get_all_attributes() 71 | { 72 | $this->getAttributes()->shouldReturn([ 73 | 'id' => 'welcome', 74 | 'class' => 'welcome big' 75 | ]); 76 | } 77 | 78 | /** @test **/ 79 | public function it_can_get_attributes_for_a_non_DOMElement() 80 | { 81 | $node = (new DOMDocument('1.0', 'utf-8')) 82 | ->createTextNode('Hello World'); 83 | 84 | $this->beConstructedWith($node); 85 | 86 | $this->getAttributes()->shouldReturn([]); 87 | } 88 | 89 | /** @test **/ 90 | public function it_confirms_child_nodes() 91 | { 92 | $node = (new DOMDocument('1.0', 'utf-8')) 93 | ->createElement('div', '

Hello World

'); 94 | 95 | $this->beConstructedWith($node); 96 | 97 | $this->hasChildren()->shouldReturn(true); 98 | } 99 | 100 | /** @test **/ 101 | public function it_confirms_absence_of_child_nodes() 102 | { 103 | $node = (new DOMDocument('1.0', 'utf-8')) 104 | ->createElement('div'); 105 | 106 | $this->beConstructedWith($node); 107 | 108 | $this->hasChildren()->shouldReturn(false); 109 | } 110 | 111 | /** @test **/ 112 | public function it_can_remove_itself_from_the_document() 113 | { 114 | $document = new DOMDocument('1.0', 'utf-8'); 115 | $parentNode = $document->createElement('div'); 116 | $node = $document->createElement('p', 'Hello World'); 117 | 118 | $parentNode->appendChild($node); 119 | $document->appendChild($parentNode); 120 | 121 | $this->beConstructedWith($node); 122 | $this->remove()->shouldReturn($node); 123 | } 124 | 125 | public function it_can_get_the_parent() 126 | { 127 | $doc = new DOMDocument('1.0', 'utf-8'); 128 | $div = $doc->createElement('div'); 129 | $p = $doc->createElement('p', 'Hello World'); 130 | $div->appendChild($p); 131 | 132 | $this->beConstructedWith($p); 133 | $this->getParent()->shouldReturnAnInstanceOf('Predmond\HtmlToAmp\Element'); 134 | } 135 | 136 | public function it_can_append_an_element(Element $element) 137 | { 138 | 139 | $doc = new DOMDocument('1.0', 'utf-8'); 140 | $div = $doc->createElement('div'); 141 | $p = $doc->createElement('p', 'Hello World'); 142 | 143 | $element->getNode()->willReturn($p); 144 | 145 | $this->beConstructedWith($div); 146 | 147 | $this->appendChild($element)->shouldReturn(null); 148 | } 149 | 150 | public function it_can_create_a_new_element_from_the_owner_document() 151 | { 152 | $doc = new DOMDocument('1.0', 'utf-8'); 153 | $node = $doc->createElement('div', 'Hello world!'); 154 | $this->beConstructedWith($node); 155 | 156 | $p = $this 157 | ->createWritableElement('p', ['class' => 'foo']) 158 | ->shouldReturnAnInstanceOf(Element::class); 159 | 160 | $this->appendChild($p)->shouldReturn(null); 161 | } 162 | 163 | public function it_can_replace_itself_in_the_document_with_another_element() 164 | { 165 | $doc = new DOMDocument('1.0', 'utf-8'); 166 | $parent = $doc->createElement('div'); 167 | $container = $doc->createElement('div', 'hello world'); 168 | $parent->appendChild($container); 169 | 170 | $this->beConstructedWith($container); 171 | 172 | $p = $this 173 | ->createWritableElement('p', ['class' => 'foo']) 174 | ->shouldReturnAnInstanceOf(Element::class); 175 | 176 | $this->replaceWith($p)->shouldReturnAnInstanceOf(\DOMElement::class); 177 | } 178 | 179 | public function it_should_not_replace_itself_if_a_parent_node_is_not_found() 180 | { 181 | $p = $this->createWritableElement('p'); 182 | 183 | $this->replaceWith($p)->shouldBe(false); 184 | } 185 | 186 | public function it_removes_itself_from_the_document() 187 | { 188 | $doc = new DOMDocument('1.0', 'utf-8'); 189 | $parent = $doc->createElement('div'); 190 | $container = $doc->createElement('div', 'hello world'); 191 | $parent->appendChild($container); 192 | 193 | $this->beConstructedWith($container); 194 | 195 | $this->remove()->shouldReturn($this->getNode()); 196 | } 197 | 198 | public function it_should_not_remove_itself_without_a_parent_node() 199 | { 200 | $this->remove()->shouldBe(false); 201 | } 202 | 203 | public function it_can_get_child_elements() 204 | { 205 | $doc = new DOMDocument('1.0', 'utf-8'); 206 | $parent = $doc->createElement('div'); 207 | $container = $doc->createElement('div', 'hello world'); 208 | $parent->appendChild($container); 209 | 210 | $this->beConstructedWith($parent); 211 | 212 | $this->getChildren()->shouldContainElements(); 213 | } 214 | 215 | public function it_should_not_return_child_elements_when_none_exist() 216 | { 217 | $node = (new DOMDocument('1.0', 'utf-8')) 218 | ->createElement('div'); 219 | 220 | $this->beConstructedWith($node); 221 | 222 | $this->getChildren()->shouldNotContainElements(); 223 | } 224 | 225 | public function getMatchers() 226 | { 227 | return [ 228 | 'containElements' => function (array $subject) { 229 | $count = 0; 230 | $total = count($subject); 231 | 232 | foreach ($subject as $node) { 233 | if ($node instanceof Element) { 234 | $count++; 235 | } 236 | } 237 | 238 | return ($count === $total && $count > 0); 239 | } 240 | ]; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /spec/EnvironmentSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($emitter); 17 | } 18 | 19 | public function it_is_initializable() 20 | { 21 | $this->shouldHaveType('Predmond\HtmlToAmp\Environment'); 22 | } 23 | 24 | public function it_can_add_a_converter( 25 | ConverterInterface $converter, 26 | Emitter $emitter 27 | ) { 28 | $pHigh = EmitterInterface::P_HIGH; 29 | $pNormal = EmitterInterface::P_NORMAL; 30 | $pLow = EmitterInterface::P_LOW; 31 | 32 | $converter 33 | ->getSubscribedEvents()->shouldBeCalled() 34 | ->willReturn([ 35 | 'img' => 'handleImg', 36 | 'convert.foo' => ['handleFoo', $pHigh], 37 | 'convert.bar' => ['handleBar', $pLow], 38 | // Bad handlers to check the automatic prepending of "convert." 39 | 'convertfizz' => ['badFizzHandler'], 40 | 'convert..buzz' => ['badBuzzHandler'], 41 | ]); 42 | 43 | $emitter 44 | ->addListener('convert.img', [$converter, 'handleImg'], $pNormal) 45 | ->shouldBeCalled(); 46 | 47 | $emitter 48 | ->addListener('convert.foo', [$converter, 'handleFoo'], $pHigh) 49 | ->shouldBeCalled(); 50 | 51 | $emitter 52 | ->addListener('convert.bar', [$converter, 'handleBar'], $pLow) 53 | ->shouldBeCalled(); 54 | 55 | $emitter 56 | ->addListener( 57 | 'convert.convertfizz', 58 | [$converter, 'badFizzHandler'], $pNormal 59 | ) 60 | ->shouldBeCalled(); 61 | 62 | $emitter 63 | ->addListener( 64 | 'convert..buzz', 65 | [$converter, 'badBuzzHandler'], $pNormal 66 | ) 67 | ->shouldBeCalled(); 68 | 69 | $this->addConverter($converter)->shouldReturn($this); 70 | } 71 | 72 | public function it_returns_the_event_emitter() 73 | { 74 | $this->getEventEmitter()->shouldReturnAnInstanceOf(Emitter::class); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/AmpConverter.php: -------------------------------------------------------------------------------- 1 | environment = $env; 15 | 16 | if ($this->environment === null) { 17 | $this->environment = Environment::createDefaultEnvironment(); 18 | } 19 | } 20 | 21 | public function convert($html) 22 | { 23 | if (trim($html) === '') { 24 | return ''; 25 | } 26 | 27 | $document = $this->createDocument($html); 28 | 29 | if (!($root = $document->getElementsByTagName('html')->item(0))) { 30 | throw new \InvalidArgumentException('Invalid HTML was provided'); 31 | } 32 | 33 | $root = new Element($root); 34 | $this->convertChildren($root); 35 | 36 | $this->environment->getEventEmitter()->emit('after.convert', $document); 37 | 38 | $this->removeProhibited($document); 39 | $this->removeProhibitedAttributes($document); 40 | 41 | $this->environment->getEventEmitter()->emit('before.sanitize', $document); 42 | 43 | $ampHtml = $this->sanitize($document->saveHTML()); 44 | 45 | return $ampHtml; 46 | } 47 | 48 | /** 49 | * @param string $html 50 | * 51 | * @return \DOMDocument 52 | */ 53 | private function createDocument($html) 54 | { 55 | $document = new DOMDocument(); 56 | 57 | libxml_use_internal_errors(true); 58 | $document->loadHTML('' . $html); 59 | $document->encoding = 'UTF-8'; 60 | libxml_clear_errors(); 61 | 62 | return $document; 63 | } 64 | 65 | private function convertChildren(ElementInterface $element) 66 | { 67 | if ($element->hasChildren()) { 68 | foreach ($element->getChildren() as $child) { 69 | $this->convertChildren($child); 70 | } 71 | } 72 | 73 | $this->convertToAmp($element); 74 | } 75 | 76 | private function convertToAmp(ElementInterface $element) 77 | { 78 | $tag = $element->getTagName(); 79 | 80 | /** @var ConverterInterface $converter */ 81 | $event = $this->environment->getEventEmitter() 82 | ->emit("convert.{$tag}", $element, $tag); 83 | } 84 | 85 | private function sanitize($html) 86 | { 87 | $html = preg_replace('/]+>/', '', $html); 88 | $unwanted = array('', '', '', '', '', '', '', ' '); 89 | $html = str_replace($unwanted, '', $html); 90 | $html = trim($html, "\n\r\0\x0B"); 91 | 92 | return $html; 93 | } 94 | 95 | private function removeProhibited(\DOMDocument $document) 96 | { 97 | // TODO: Config-based 98 | $xpath = '//' . implode('|//', [ 99 | 'base', 100 | 'frame', 101 | 'frameset', 102 | 'object', 103 | 'param', 104 | 'applet', 105 | 'embed', 106 | 'form', 107 | 'input', 108 | 'textarea', 109 | 'script', 110 | 'select', 111 | 'option', 112 | 'meta', 113 | 'map', 114 | ]); 115 | 116 | $elements = (new \DOMXPath($document))->query($xpath); 117 | 118 | /** @var \DOMElement $element */ 119 | foreach ($elements as $element) { 120 | if ($element->nodeName === 'meta' && $element->getAttribute('http-equiv') === '') { 121 | continue; 122 | } 123 | 124 | if ($element->parentNode !== null) { 125 | $element->parentNode->removeChild($element); 126 | } 127 | } 128 | 129 | // Remove anchors with javascript in the href 130 | $anchors = (new \DOMXPath($document)) 131 | ->query('//a[contains(@href, "javascript:")]'); 132 | 133 | foreach ($anchors as $a) { 134 | if ($a->parentNode !== null) { 135 | $a->parentNode->removeChild($a); 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Removed prohibited attributes 142 | * 143 | * @param \DOMDocument $document 144 | */ 145 | private function removeProhibitedAttributes(\DOMDocument $document) 146 | { 147 | // Globally invalid attributes 148 | // @todo more globally invalid attribute research. Does AMP have documentation for this? 149 | $invalidAttributes = [ 150 | 'align', 151 | 'border', 152 | 'contenteditable', 153 | 'style', 154 | ]; 155 | 156 | /** 157 | * Example xpath: "//*[@align]|//*[@style]" 158 | */ 159 | $xpath = '//*[@' . implode(']|//*[@', $invalidAttributes) . ']'; 160 | $elements = (new \DOMXPath($document))->query($xpath); 161 | 162 | /** @var \DOMElement $element */ 163 | foreach ($elements as $element) { 164 | $this->removeElementAttributes($element, $invalidAttributes); 165 | } 166 | 167 | // Accepted attributes that are not valid on specific elements 168 | $xpathQueries = [ 169 | '//table[@border]|//table[@cellpadding]|//table[@cellspacing]|//table[@width]' => ['border', 'cellpadding', 'cellspacing', 'width'], 170 | '//td[@width]|//td[@height]' => ['width', 'height'], 171 | '//ul|//ol' => ['compact', 'reversed', 'start', 'type'], 172 | '//blockquote[@cite]' => ['cite'], 173 | ]; 174 | 175 | foreach ($xpathQueries as $query => $attributes) { 176 | $elements = (new \DOMXPath($document))->query($query); 177 | foreach ($elements as $element) { 178 | $this->removeElementAttributes($element, $attributes); 179 | } 180 | } 181 | } 182 | 183 | private function removeElementAttributes( 184 | \DOMElement $node, 185 | array $attributes = [] 186 | ) { 187 | foreach ($attributes as $attribute) { 188 | $node->removeAttribute($attribute); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Converter/ConverterInterface.php: -------------------------------------------------------------------------------- 1 | Converter using Cloudinary 14 | * 15 | * @package Predmond\HtmlToAmp\Converter\Extensions 16 | * @see http://cloudinary.com/ 17 | */ 18 | class CloudinaryImageConverter implements ConverterInterface 19 | { 20 | /** 21 | * @param ElementInterface $element 22 | * 23 | * @return string 24 | */ 25 | public function handleTagImg(EventInterface $event, ElementInterface $element) 26 | { 27 | 28 | // Should mutate all images and stop propagation 29 | if (function_exists('cloudinary_url')) { 30 | $src = $element->getAttribute('src'); 31 | $width = $element->getAttribute('width'); 32 | $height = $element->getAttribute('height'); 33 | $sizes = $this->generateSizes(); 34 | $srcset = $this->generateSrcset($src); 35 | 36 | $urlConfig = [ 37 | 'width' => 400, 38 | 'height' => 225, 39 | 'crop' => 'limit', 40 | 'sign_url' => true, 41 | 'type' => 'fetch', 42 | 'effect' => 'sharpen', 43 | ]; 44 | 45 | if ($width < 100 || $height < 100) { 46 | $width = $urlConfig['width']; 47 | $height = $urlConfig['height']; 48 | } else { 49 | $urlConfig['width'] = $width; 50 | $urlConfig['height'] = $height; 51 | } 52 | 53 | // Create the responsive tag 54 | $ampImg = $element->createWritableElement('amp-img'); 55 | $ampImg->setAttribute( 56 | 'src', 57 | cloudinary_url($src, $urlConfig) 58 | ); 59 | $ampImg->setAttribute('width', $width); 60 | $ampImg->setAttribute('height', $height); 61 | $ampImg->setAttribute('sizes', $sizes); 62 | $ampImg->setAttribute('srcset', $srcset); 63 | $ampImg->setAttribute('layout', 'responsive'); 64 | $ampImg->setAttribute('alt', $element->getAttribute('alt')); 65 | $ampImg->setAttribute('attribution', $element->getAttribute('attribution')); 66 | $ampImg->setAttribute('class', 'amp-img'); 67 | $element->replaceWith($ampImg); 68 | 69 | $event->stopPropagation(); 70 | } 71 | } 72 | 73 | public function getSubscribedEvents() 74 | { 75 | return [ 76 | 'img' => ['handleTagImg', EmitterInterface::P_HIGH] 77 | ]; 78 | } 79 | 80 | /** 81 | * Generate the Attribute 82 | * 83 | * @param string $src the original source image 84 | * @return string 85 | */ 86 | private function generateSrcset($src) 87 | { 88 | return implode(', ', [ 89 | cloudinary_url($src, [ 90 | 'width' => 700, 91 | 'crop' => 'limit', 92 | 'sign_url' => true, 93 | 'type' => 'fetch', 94 | 'effect' => 'sharpen' 95 | ]) . ' 700w', 96 | cloudinary_url($src, [ 97 | 'width' => 465, 98 | 'crop' => 'limit', 99 | 'sign_url' => true, 100 | 'type' => 'fetch', 101 | 'effect' => 'sharpen' 102 | ]) . ' 465w', 103 | ]); 104 | } 105 | 106 | /** 107 | * Get attribute value 108 | * 109 | * @return string 110 | */ 111 | private function generateSizes() 112 | { 113 | return implode(', ', [ 114 | '(min-width: 600px) 600px', 115 | '100vw' 116 | ]); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Converter/Extensions/InstagramConverter.php: -------------------------------------------------------------------------------- 1 | hasChildren() == false) { 14 | return false; 15 | } 16 | 17 | $hasClass = in_array('instagram-media', explode(' ', $element->getAttribute('class'))); 18 | $hasAttr = array_key_exists('data-instgrm-version', $element->getAttributes()); 19 | 20 | return $hasClass || $hasAttr; 21 | } 22 | 23 | public function getEmbedShortcode($element) 24 | { 25 | $href = $element->getAttribute('href'); 26 | if ($href != '') { 27 | if (1 === preg_match('/(?:instagr\.am|instagram\.com)\/p\/([^\/]+)\/?$/i', $href, $matches)) { 28 | return $matches[1]; 29 | } 30 | } 31 | 32 | foreach($element->getChildren() as $child) { 33 | $shortcode = $this->getEmbedShortcode($child); 34 | if ($shortcode !== null) { 35 | return $shortcode; 36 | } 37 | } 38 | 39 | return null; 40 | 41 | } 42 | 43 | public function handleInstagram( EventInterface $event, ElementInterface $element) { 44 | 45 | if ($this->convertToAmp($element) == false) { 46 | return; 47 | } 48 | 49 | $shortcode = $this->getEmbedShortcode($element); 50 | 51 | if ($shortcode == null) { 52 | return; 53 | } 54 | 55 | $attrs = [ 56 | 'layout' => "responsive", 57 | 'width' => 600, 58 | 'height' => 384, 59 | 'data-shortcode' => $shortcode 60 | ]; 61 | 62 | $container = $element->createWritableElement('div', ['class' => 'instagram-container']); 63 | $ampInstagram = $element->createWritableElement('amp-instagram', $attrs); 64 | $container->appendChild($ampInstagram); 65 | 66 | $element->replaceWith($container); 67 | $event->stopPropagation(); 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function getSubscribedEvents() 74 | { 75 | return ['blockquote' => ['handleInstagram']]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Converter/Extensions/TwitterConverter.php: -------------------------------------------------------------------------------- 1 | getAttribute('class')); 20 | 21 | if ( 22 | in_array('twitter-tweet', $classAttr) 23 | && false !== $twitterStatusId = $this->getStatusId($element) 24 | ) { 25 | $container = $element->createWritableElement('div', ['class' => 'amp-twitter-container']); 26 | $container->appendChild($this->createAmpTwitterTag($element, $twitterStatusId)); 27 | $element->replaceWith($container); 28 | $event->stopPropagation(); 29 | } 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getSubscribedEvents() 36 | { 37 | return [ 38 | 'blockquote' => 'handleBlockquote' 39 | ]; 40 | } 41 | 42 | /** 43 | * Extract a Twitter Status ID from a blockquote 44 | * @param $element 45 | * @return mixed|bool|string 46 | */ 47 | private function getStatusId(ElementInterface $element) 48 | { 49 | foreach ($element->getChildren() as $child) { 50 | if (preg_match('/status(?:es)?\/(\d+)/i', $child->getAttribute('href'), $match)) { 51 | return $match[1]; 52 | } 53 | } 54 | 55 | return false; 56 | } 57 | 58 | /** 59 | * Create an amp-twitter tag 60 | * 61 | * @param ElementInterface $element 62 | * @param $twitterStatusId 63 | * @return ElementInterface 64 | */ 65 | private function createAmpTwitterTag(ElementInterface $element, $twitterStatusId) 66 | { 67 | return $element->createWritableElement('amp-twitter', [ 68 | 'layout' => 'responsive', 69 | 'data-tweetid' => $twitterStatusId, 70 | 'width' => 390, 71 | 'height' => 330, 72 | 'data-cards' => 'hidden' 73 | ]); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Converter/Extensions/YoutubeConverter.php: -------------------------------------------------------------------------------- 1 | getAttribute('src'); 16 | if (1 === preg_match('/youtube\.com\/(?:v|embed)\/([a-zA-z0-9_-]+)/i', $src, $match)) { 17 | $container = $element->createWritableElement('div', ['class' => 'youtube-container']); 18 | $container->appendChild($this->createAmpTag($element, $match[1])); 19 | $element->replaceWith($container); 20 | $event->stopPropagation(); 21 | } 22 | } 23 | 24 | public function handleObject(EventInterface $event, ElementInterface $element) 25 | { 26 | $embedCode = false; 27 | 28 | /** @var ElementInterface $child */ 29 | foreach ($element->getChildren() as $child) { 30 | if (1 === preg_match('/youtube\.com\/(?:v|embed)\/([a-zA-z0-9_-]+)/i', $child->getAttribute('value'), $match)) { 31 | $embedCode = $match[1]; 32 | } 33 | } 34 | 35 | if ($embedCode !== false) { 36 | 37 | $container = $element->createWritableElement('div', [ 38 | 'class' => 'youtube-container' 39 | ]); 40 | 41 | $container->appendChild($this->createAmpTag($element, $embedCode)); 42 | $element->replaceWith($container); 43 | $event->stopPropagation(); 44 | } 45 | } 46 | 47 | /** 48 | * @param ElementInterface $element 49 | * @param $embedCode 50 | * @return ElementInterface 51 | */ 52 | private function createAmpTag(ElementInterface $element, $embedCode) 53 | { 54 | return $element->createWritableElement('amp-youtube', [ 55 | 'data-videoid' => $embedCode, 56 | 'layout' => 'responsive', 57 | // 16:9 Ratio 58 | 'width' => '560', 59 | 'height' => '315' 60 | ]); 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | public function getSubscribedEvents() 67 | { 68 | return [ 69 | 'iframe' => ['handleIframe', Emitter::P_HIGH], 70 | 'object' => ['handleObject', Emitter::P_HIGH] 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Converter/ImageConverter.php: -------------------------------------------------------------------------------- 1 | createWritableElement('amp-img'); 18 | 19 | foreach ($this->validAttributes as $attribute) { 20 | if ($element->getAttribute($attribute)) { 21 | $ampImg->setAttribute( 22 | $attribute, 23 | $element->getAttribute($attribute) 24 | ); 25 | } 26 | } 27 | 28 | return $element->replaceWith($ampImg); 29 | } 30 | 31 | public function getSubscribedEvents() 32 | { 33 | return [ 34 | 'img' => ['handleTagImg'] 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Element.php: -------------------------------------------------------------------------------- 1 | node = $node; 18 | } 19 | 20 | public function getNode() 21 | { 22 | return $this->node; 23 | } 24 | 25 | public function getTagName() 26 | { 27 | return $this->node->nodeName; 28 | } 29 | 30 | public function getValue() 31 | { 32 | return $this->node->nodeValue; 33 | } 34 | 35 | public function getAttribute($attributeName) 36 | { 37 | if ($this->node instanceof DOMElement) { 38 | return $this->node->getAttribute($attributeName); 39 | } 40 | 41 | return ''; 42 | } 43 | 44 | public function setAttribute($attributeName, $attributeValue) 45 | { 46 | if ($this->node instanceof DOMElement) { 47 | $this->node->setAttribute($attributeName, $attributeValue); 48 | } 49 | } 50 | 51 | public function getAttributes() 52 | { 53 | $data = []; 54 | 55 | if ( 56 | $this->node instanceof DOMElement 57 | && $this->node->hasAttributes() 58 | ) { 59 | foreach ($this->node->attributes as $attribute) { 60 | $data[$attribute->nodeName] = $attribute->nodeValue; 61 | } 62 | } 63 | 64 | return $data; 65 | } 66 | 67 | /** 68 | * @return ElementInterface|null 69 | */ 70 | public function getParent() 71 | { 72 | return new static($this->node->parentNode) ?: null; 73 | } 74 | 75 | /** 76 | * @return bool 77 | */ 78 | public function hasChildren() 79 | { 80 | return $this->node->hasChildNodes(); 81 | } 82 | 83 | /** 84 | * Append an Element as a child of the current element 85 | * 86 | * @param Element $element The element to append as a child 87 | * @return null 88 | */ 89 | public function appendChild(ElementInterface $element) 90 | { 91 | $this->getNode()->appendChild($element->getNode()); 92 | } 93 | 94 | /** 95 | * @param string $elementName 96 | * @param array $attributes 97 | * @return static 98 | */ 99 | public function createWritableElement($elementName, array $attributes = []) 100 | { 101 | $element = $this->node->ownerDocument->createElement($elementName); 102 | 103 | if ($element instanceof \DOMElement) { 104 | foreach ($attributes as $attribute => $value) { 105 | $element->setAttribute($attribute, $value); 106 | } 107 | } 108 | 109 | return new static($element); 110 | } 111 | 112 | /** 113 | * Replace current element with a new DOMNode 114 | * 115 | * @param $node 116 | * @return DOMNode|bool 117 | */ 118 | public function replaceWith(ElementInterface $element) 119 | { 120 | if ($this->node->parentNode !== null) { 121 | return $this->node->parentNode->replaceChild( 122 | $element->getNode(), 123 | $this->getNode() 124 | ); 125 | } 126 | 127 | return false; 128 | } 129 | 130 | /** 131 | * Remove this element from the referenced DOMDocument 132 | * 133 | * @return mixed 134 | */ 135 | public function remove() 136 | { 137 | if ($this->node->parentNode) { 138 | return $this->node->parentNode->removeChild($this->node); 139 | } 140 | 141 | return false; 142 | } 143 | 144 | /** 145 | * @return ElementInterface[] 146 | */ 147 | public function getChildren() 148 | { 149 | $children = []; 150 | 151 | if ($this->node->childNodes->length > 0) { 152 | /** @var \DOMNode $node */ 153 | foreach ($this->node->childNodes as $node) { 154 | $children[] = new static($node); 155 | } 156 | } 157 | 158 | return $children; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/ElementInterface.php: -------------------------------------------------------------------------------- 1 | eventEmitter = $eventEmitter; 22 | 23 | if ($this->eventEmitter === null) { 24 | $this->eventEmitter = new Emitter(); 25 | } 26 | } 27 | 28 | public static function createDefaultEnvironment() 29 | { 30 | $env = new static(); 31 | $env->addConverter(new ImageConverter()); 32 | 33 | return $env; 34 | } 35 | 36 | public function addConverter(ConverterInterface $converter) 37 | { 38 | foreach ($converter->getSubscribedEvents() as $tag => $event) { 39 | $eventName = stripos($tag, 'convert.') === 0 ? 40 | $tag : "convert.{$tag}"; 41 | 42 | if (is_string($event)) { 43 | $event = [$event]; 44 | } 45 | 46 | $event = array_values($event); 47 | list($callbackName, $priority) = count($event) > 1 ? 48 | [$event[0], $event[1]] : [$event[0], EmitterInterface::P_NORMAL]; 49 | 50 | $this->eventEmitter->addListener( 51 | $eventName, 52 | [$converter, $callbackName], 53 | $priority 54 | ); 55 | } 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @return Emitter|EmitterInterface 62 | */ 63 | public function getEventEmitter() 64 | { 65 | return $this->eventEmitter; 66 | } 67 | } 68 | --------------------------------------------------------------------------------