Skip to content

Instantly share code, notes, and snippets.

@Machy8
Last active July 30, 2019 20:13
Show Gist options
  • Save Machy8/4c41287b9fa0e27cecfd96395b2b280e to your computer and use it in GitHub Desktop.
Save Machy8/4c41287b9fa0e27cecfd96395b2b280e to your computer and use it in GitHub Desktop.
Symfony Twig filters for converting content to AMP valid content

Requires Nette/Utils

composer require nette/utils
<?php
declare(strict_types = 1);
namespace App\Twig\Extensions;
use Nette\Utils\Html;
use Nette\Utils\Image;
use Nette\Utils\Validators;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
final class AmpFiltersExtension extends AbstractExtension
{
private const EVENT_ATTRS_REGULAR_EXPRESSION =
'/(<[^!<]+)\s(on[a-zA-Z]+\s*=\s*(?:([\'"])(?!\3).+?\3|(?:\S+?\(.*?\)(?=[\s>]))))(.*?>)/';
private const IFRAME_REGULAR_EXPRESSION = '/<iframe (?<attributes>.*)>.*<\/iframe>/';
private const IMAGE_REGULAR_EXPRESSION = '/<img (?<attributes>[^>]+)>/';
private const ATTRIBUTES_TO_REMOVE_REGULAR_EXPRESSION =
'/(?:style|align|target|frame|scope|border|xml:lang|lang|aria-level|role|type)='
. '("([^"]*)"|\'([^\']*)\')/';
private const TAGS_TO_REMOVE_REGULAR_EXPRESSION =
'/<\/?(?object|video|font|meta|ins|style)(?: [^>]*)?>/';
private const NORMAL_IFRAME_TYPE = 'normalIframe';
private const YOUTUBE_IFRAME_TYPE = 'youtubeIframe';
private const HEADERS_200_STATE_CODE = '200 OK';
private const YOUTUBE_IFRAME_TYPE_KEYWORD = 'youtube.com';
/**
* @var ContainerInterface
*/
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getFilters(): array
{
return [
new TwigFilter('convertToAmpValidContent', function (string $content, array $parameters = []): string {
return $this->toAmpValidContent($content, $parameters);
}),
new TwigFilter('convertImagesToAmpImages', function (
string $content,
bool $enableLightbox = false
): string {
return $this->convertImagesToAmpImages($content, $enableLightbox);
}),
new TwigFilter('convertToAmpImage', function (
string $tag,
bool $enableLightbox = false,
int $width = NULL,
int $height = NULL
): Html {
return $this->convertToAmpImage($tag, $enableLightbox, $width, $height);
}),
new TwigFilter('removeDisallowedAttributesAndTags', function (string $content): string {
return $this->removeDisallowedAttributesAndTags($content);
}),
new TwigFilter('removeEventAttributes', function (string $content): string {
return $this->removeEventAttributes($content);
})
];
}
private function toAmpValidContent(string $content, array $parameters = []): string
{
$content = $this->convertImagesToAmpImages($content, $parameters['imagesLightboxEnabled'] ?? false);
$content = $this->convertIframesToAmpIframes($content);
return $content;
}
private function convertIframesToAmpIframes(string $content): string
{
$iframeMatches = [];
self::matchIframeTags($content, $iframeMatches);
foreach ($iframeMatches as $iframe) {
$ampIframe = $this->convertToAmpIframe($iframe[0]);
$content = preg_replace('/' . preg_quote($iframe[0], '/') . '/', $ampIframe, $content, 1);
}
return $content;
}
private function convertImagesToAmpImages(string $content, bool $enableLightbox = false): string
{
$imagesMatches = [];
preg_match_all(self::IMAGE_REGULAR_EXPRESSION, $content, $imagesMatches, PREG_SET_ORDER);
foreach ($imagesMatches as $image) {
$ampImage = $this->convertToAmpImage($image[0], $enableLightbox);
$content = preg_replace('/' . preg_quote($image[0], '/') . '/', $ampImage, $content, 1);
}
return $content;
}
private function convertToAmpIframe(string $tag): Html
{
$ampIframeTag = $this->createHtmlElement(self::IFRAME_REGULAR_EXPRESSION, 'amp-iframe', $tag);
$ampIframeSrcAttribute = $ampIframeTag->getAttribute('src');
if (self::isYoutubeIframeType($ampIframeSrcAttribute)) {
$ampIframeSrcAttributeToAray = explode('/', $ampIframeSrcAttribute);
$ampIframeTag->removeAttribute('src');
$ampIframeTag->removeAttribute('frameborder');
$ampIframeTag->removeAttribute('frame');
$ampIframeTag->removeAttribute('allow');
$ampIframeTag->removeAttribute('allowfullscreen');
$ampIframeTag->removeAttribute('scrolling');
$ampIframeTag->setName('amp-youtube');
$videoId = end($ampIframeSrcAttributeToAray);
if (is_string($videoId)) {
$videoId = explode('?', $videoId);
$videoId = $videoId[0];
$ampIframeTag->setAttribute('data-videoid', $videoId);
}
} else {
$ampIframeTag->appendAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
}
return $ampIframeTag;
}
private function convertToAmpImage(
string $tag,
bool $enableLightbox = false,
?int $width = null,
?int $height = null
): Html {
$ampImageTag = $this->createHtmlElement(self::IMAGE_REGULAR_EXPRESSION, 'amp-img', $tag);
$ampImagePath = $ampImageTag->getAttribute('src');
$ampImageDirectoryPath = $this->container->getParameter('kernel.public_dir') . $ampImagePath;
$ampImageTagFallbackWrapper = Html::el('noscript');
$ampImageTagFallback = Html::el('img')->setAttribute('src', $ampImagePath);
$ampImageTagStyleAttribute = $ampImageTag->getAttribute('style');
$ampImageWidth = null;
$ampImageHeight = null;
$ampImageTag->removeAttributes(['style, align, alt, title']);
$ampImageTag->setAttribute('alt', '');
if (
(bool) $ampImageTagStyleAttribute
&& (bool) preg_match('/width: (?<size>[\d]+px|%);?/', $ampImageTagStyleAttribute, $widthMatches)
&& (bool) preg_match('/height: (?<size>[\d]+px|%);?/', $ampImageTagStyleAttribute, $heightMatches)
) {
$ampImageWidth = $widthMatches['size'];
$ampImageHeight = $heightMatches['size'];
} elseif (file_exists($ampImageDirectoryPath)) {
$ampImage = Image::fromFile($ampImageDirectoryPath);
$ampImageWidth = $ampImage->width;
$ampImageHeight = $ampImage->height;
} elseif ($this->imageOnUrlExists($ampImagePath) && (bool) getimagesize($ampImagePath)) {
$size = getimagesize($ampImagePath);
$ampImageWidth = $size[0];
$ampImageHeight = $size[1];
}
if ($width && $height) {
$ampImageWidth = $width;
$ampImageHeight = $height;
}
if ((bool) $ampImageWidth && (bool) $ampImageHeight) {
$sizeAttributes = [
'width' => $ampImageWidth,
'height' => $ampImageHeight
];
$ampImageTagFallback->addAttributes($sizeAttributes);
$ampImageTag->addAttributes($sizeAttributes);
} else {
$ampImageTag->setAttribute('layout', 'nodisplay');
}
if ($enableLightbox) {
$ampImageTag->addAttributes([
'on' => 'tap:lightbox',
'role' => 'button',
'tabindex' => 0
]);
}
return $ampImageTag->setHtml($ampImageTagFallbackWrapper->setHtml($ampImageTagFallback));
}
public static function matchIframeTags(?string $content, ?array &$matches = null, ?array &$types = null): bool
{
if ( ! is_array($matches)) {
$matches = [];
}
if (!is_array($types)) {
$types = [];
}
if (!$content) {
return false;
}
preg_match_all(self::IFRAME_REGULAR_EXPRESSION, $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$isYoutubeIframeType = self::isYoutubeIframeType($match[0]);
if ($isYoutubeIframeType) {
if (self::youtubeIframeTypeFounded($types)) {
continue;
}
$types[] = self::YOUTUBE_IFRAME_TYPE;
} elseif (!self::normalIframeTypeFounded($types)) {
$types[] = self::NORMAL_IFRAME_TYPE;
}
}
return (bool) $matches;
}
public static function normalIframeTypeFounded(array $types): bool
{
return in_array(self::NORMAL_IFRAME_TYPE, $types, true);
}
public static function youtubeIframeTypeFounded(array $types): bool
{
return in_array(self::YOUTUBE_IFRAME_TYPE, $types, true);
}
private function createHtmlElement(string $regularExpression, string $newTagName, string $tag): Html
{
$tag = preg_replace($regularExpression, $newTagName . ' $1', $tag, 1);
$tag = Html::el($tag)->appendAttribute('layout', 'responsive');
return $tag;
}
private function imageOnUrlExists(string $url): bool
{
if ( ! Validators::isUrl($url)) {
return false;
}
$headers = get_headers($url);
return is_array($headers)
&& (bool) count($headers)
&& stripos($headers[0], self::HEADERS_200_STATE_CODE) !== false;
}
private static function isYoutubeIframeType(string $content): bool
{
if ( ! (bool) $content) {
return false;
}
return strpos($content, self::YOUTUBE_IFRAME_TYPE_KEYWORD) !== false;
}
private function removeDisallowedAttributesAndTags(string $content): string
{
if ( ! $content) {
return '';
}
$content = preg_replace(self::TAGS_TO_REMOVE_REGULAR_EXPRESSION, '', $content);
$content = preg_replace(self::ATTRIBUTES_TO_REMOVE_REGULAR_EXPRESSION, '', $content);
return $content;
}
private function removeEventAttributes(string $content): string
{
$eventMatches = [];
preg_match_all(self::EVENT_ATTRS_REGULAR_EXPRESSION, $content, $eventMatches, PREG_SET_ORDER);
foreach ($eventMatches as $eventMatch) {
$elWithEvent = reset($eventMatch);
$el = preg_replace(
'/\s(on[a-zA-Z]+\s*=\s*(?:([\'"])(?!\2).+?\2|(?:\S+?\(.*?\)(?=[\s>]))))/',
'',
$elWithEvent
);
$content = str_replace($elWithEvent, $el, $content);
}
return $content;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment