Skip to content

Commit

Permalink
Merge pull request #84 from Lullabot/83-layout-validation
Browse files Browse the repository at this point in the history
83 layout validation
  • Loading branch information
sidkshatriya committed Jun 2, 2016
2 parents 35d061c + 6972c71 commit faf08b6
Show file tree
Hide file tree
Showing 13 changed files with 1,436 additions and 18 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ Open source PHP library and console utility to convert HTML to [AMP HTML](https:

The AMP PHP Library is an open source and pure PHP Library that:
- Works with whole or partial HTML documents (or strings). Specifically, the AMP PHP Library:
- Reports compliance of a whole/partial HTML document with the [AMP HTML specification](https://www.ampproject.org/). We implement an AMP HTML validator in pure PHP to report compliance of an arbitrary HTML document / HTML fragment with the AMP HTML standard. This validator is a ported subset of the [canonical validator](https://github.com/ampproject/amphtml/tree/master/validator) that is implemented in JavaScript. In particular this PHP validator does not (yet) support template, cdata, css and layout validation. Otherwise, it supports tag specification validation, attribute specification validation and attribute property value pair validation. It will report tags and attributes that are missing, illegal, mandatory according to spec but not present, unique according to spec but multiply present, having wrong parents or ancestors and so forth.
- Using the feedback given by the validator, tries to "correct" some issues found in the HTML to make it more AMP HTML compliant. This would involve removing:
- Reports compliance of a whole/partial HTML document with the [AMP HTML specification](https://www.ampproject.org/). We implement an AMP HTML validator in pure PHP to report compliance of an arbitrary HTML document / HTML fragment with the AMP HTML standard. This validator is a ported subset of the [canonical validator](https://github.com/ampproject/amphtml/tree/master/validator) that is implemented in JavaScript
- Specfically, the PHP validator supports tag specification validation, attribute specification validation, CDATA validation, CSS validation, Layout validation and attribute property value pair validation. It will report tags and attributes that are missing, illegal, mandatory according to spec but not present, unique according to spec but multiply present, having wrong parents or ancestors or children and so forth. It does not (yet) support template validation.
- Please note that while the AMP PHP library (already) supports many of the features and capabilities of the canonical validator, it is not intended to achieve parity in _every_ respect with the canonical validator.
- Using the feedback given by the in-house PHP validator, the AMP PHP library tries to "correct" some issues found in the HTML to make it more AMP HTML compliant. This would, for example, involve removing:
- Illegal attributes e.g. `style` within `<body>` tag
- Illegal tags e.g. `<script>` within `<body>` tag
- Illegal property value pairs e.g. remove `minimum-scale=hello` from `<meta name="viewport" content="minimum-scale=hello">`
- _Notes_:
- The "correction" of the input HTML to make it more compliant with the AMP HTML standard is currently basic. The library does a decent job of _removing_ bad things but does not _add_ tags, attributes or property-value pairs where it could "fix" things
- The library needs to be provided with well formed html. Please don't give it faulty, incorrect html (e.g. non closed `<div>` tags etc). The correction it does is related to AMP HTML standard issues only. Use a HTML tidying library if you expect your HTML to be malformed.
- The library needs to be provided with well formed HTML / HTML5. Please don't give it faulty, incorrect html (e.g. non closed `<div>` tags etc). The correction it does is related to AMP HTML standard issues only. Use a HTML tidying library if you expect your HTML to be malformed.
- Converts some non-amp elements to their AMP equivalents automatically
- An `<img>` tag is automatically converted to an `<amp-img>` tag
- An `<iframe>` tag is converted to an `<amp-iframe>`
Expand Down
67 changes: 67 additions & 0 deletions src/Validate/CssLengthAndUnit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/*
* Copyright 2016 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Lullabot\AMP\Validate;

/**
* Class CssLengthAndUnit
* @package Lullabot\AMP\Validate
*
* This class is a straight PHP port of the Context class in validator.js
* (see https://github.com/ampproject/amphtml/blob/master/validator/validator.js )
*/
class CssLengthAndUnit
{
/** @var bool */
public $is_valid = false;
/** @var bool */
public $is_set = false;
/** @var bool */
public $is_auto = false;
/** @var string */
public $unit = 'px';

/**
* CssLengthAndUnit constructor.
* @param string|null $input
* @param boolean $allow_auto
*/
public function __construct($input, $allow_auto)
{
if (empty($input)) {
$this->is_valid = true;
} else {
$this->is_set = true;
if ($input === 'auto') {
$this->is_auto = true;
$this->is_valid = $allow_auto;
} else {
$regex = '/^\d+(?:\.\d+)?(px|em|rem|vh|vw|vmin|vmax)?$/';
$matches = [];
if (preg_match($regex, $input, $matches)) {
$this->is_valid = true;
if (!empty($matches[1])) {
$this->unit = $matches[1];
} else {
$this->unit = 'px';
}
}
}
}
}
}

223 changes: 213 additions & 10 deletions src/Validate/ParsedTagSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace Lullabot\AMP\Validate;

use Lullabot\AMP\Spec\AmpLayout;
use Lullabot\AMP\Spec\AmpLayoutLayout;
use Lullabot\AMP\Spec\AttrList;
use Lullabot\AMP\Spec\AttrSpec;
use Lullabot\AMP\Spec\TagSpec;
Expand All @@ -26,17 +28,13 @@
* Class ParsedTagSpec
* @package Lullabot\AMP\Validate
*
* This class is a straight PHP port of the ParsedTagSpec class in validator.js
* This class is a straight PHP port of the ParsedTagSpec class in validator.js (also called the "canonical validator")
* (see https://github.com/ampproject/amphtml/blob/master/validator/validator.js )
*
* The static methods getAttrsFor(), getDetailOrName(), shouldRecordTagspecValidated() are normal top-level functions
* in validator.js but have been incorporated into this class, when they were ported, for convenience.
*
* Note:
* - shouldRecordTagspecValidated() in validator.js has been renamed shouldRecordTagspecValidatedTest() to prevent
* a name collision in this class
* - getAttrsFor() static method is called GetAttrsFor() in validator.js
*
* Additionally, various top level functions from the canonical validator have been ported and incorporate as static
* methods in this class. This was done to give these javascript functions a place to reside (rather than have global
* scope PHP functions). If a static method corresponds to a top level function from the canonical validator, a note of
* this is made in the docblock for that static method.
*/
class ParsedTagSpec
{
Expand All @@ -58,6 +56,8 @@ class ParsedTagSpec
protected $dispatch_key_attr_spec = null;
/** @var TagSpec[] */
protected $implicit_attr_specs = [];
/** @var string[] */
protected static $all_layouts = [];

/**
* ParsedTagSpec constructor.
Expand Down Expand Up @@ -238,7 +238,10 @@ public function validateAttrNotFoundInSpec($attr_name, Context $context, SValida
*/
public function validateAttributes(Context $context, array $encountered_attrs, SValidationResult $result_for_attempt)
{
// skip layout validation for now
if (!empty($this->spec->amp_layout)) {
$this->validateLayout($context, $encountered_attrs, $result_for_attempt);
// Continue on, regardless of whether we have failure or success; different from canonical validator
}

/** @var \SplObjectStorage $mandatory_attrs_seen */
$mandatory_attrs_seen = new \SplObjectStorage(); // Treat as a set of objects
Expand Down Expand Up @@ -378,6 +381,8 @@ public function validateAttributes(Context $context, array $encountered_attrs, S
}

/**
* This static method corresponds to top level function GetAttrsFor() in the canonical validator i.e. validator.js
*
* @param TagSpec $tag_spec
* @param AttrList[] $attr_lists_by_name
* @return AttrSpec[]
Expand Down Expand Up @@ -446,8 +451,107 @@ public static function getAttrsFor(TagSpec $tag_spec, array $attr_lists_by_name)
return $attr_specs;
}

/**
* @return string[]
*/
public static function getAllLayouts()
{
if (empty(self::$all_layouts)) {
$reflection_class = new \ReflectionClass('Lullabot\AMP\Spec\AmpLayoutLayout');
self::$all_layouts = $reflection_class->getConstants();
}

return self::$all_layouts;
}

/**
* This static method corresponds to the top level function parseLayout() in the canonical validator i.e. validator.js
*
* @param $layout
* @return string
*/
public static function parseLayout($layout)
{
if (empty($layout)) {
return AmpLayoutLayout::UNKNOWN;
}

$all_layouts = self::getAllLayouts();
$norm_layout = str_replace('-', '_', mb_strtoupper($layout, 'UTF-8'));
if (isset($all_layouts[$norm_layout])) {
return $norm_layout;
}

return AmpLayoutLayout::UNKNOWN;
}

/**
* This static method corresponds to the top level function CalculateWidth() in the canonical validator i.e. validator.js
*
* @param AmpLayout $spec
* @param string $input_layout
* @param CssLengthAndUnit $input_width
* @return CssLengthAndUnit
*/
public static function calculateWidth(AmpLayout $spec, $input_layout, CssLengthAndUnit $input_width)
{
if (($input_layout === AmpLayoutLayout::UNKNOWN ||
$input_layout === AmpLayoutLayout::FIXED) &&
!$input_width->is_set && $spec->defines_default_width
) {
return new CssLengthAndUnit('1px', false);
}
return $input_width;
}

/**
* This static method corresponds to the top level function CalculateHeight() in the canonical validator i.e. validator.js
*
* @param AmpLayout $spec
* @param string $input_layout
* @param CssLengthAndUnit $input_height
* @return CssLengthAndUnit
*/
public static function calculateHeight(AmpLayout $spec, $input_layout, CssLengthAndUnit $input_height)
{
if (in_array($input_layout, [AmpLayoutLayout::UNKNOWN, AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT])
&& !$input_height->is_set && $spec->defines_default_height
) {
return new CssLengthAndUnit('1px', false);
}
return $input_height;
}

/**
* This static method corresponds to the top level function CalculateLayout() in the canonical validator i.e. validator.js
*
* @param string $input_layout
* @param CssLengthAndUnit $width
* @param CssLengthAndUnit $height
* @param string|null $sizes_attr
* @param string|null $heights_attr
* @return string
*/
public static function calculateLayout($input_layout, CssLengthAndUnit $width, CssLengthAndUnit $height, $sizes_attr, $heights_attr)
{
if ($input_layout !== AmpLayoutLayout::UNKNOWN) {
return $input_layout;
} else if (!$width->is_set && !$height->is_set) {
return AmpLayoutLayout::CONTAINER;
} else if ($height->is_set && (!$width->is_set || $width->is_auto)) {
return AmpLayoutLayout::FIXED_HEIGHT;
} else if ($height->is_set && $width->is_set &&
(!empty($sizes_attr) || !empty($heights_attr))
) {
return AmpLayoutLayout::RESPONSIVE;
} else {
return AmpLayoutLayout::FIXED;
}
}

/**
* This static method corresponds to the top level function getTagSpecName() in the canonical validator i.e. validator.js
*
* @param TagSpec $tag_spec
* @return string
*/
Expand All @@ -457,6 +561,8 @@ public static function getTagSpecName(TagSpec $tag_spec)
}

/**
* This static method corresponds to the top level function shouldRecordTagspecValidated() in validator.js
*
* @param TagSpec $tag_spec
* @param array $detail_or_names_to_track
* @return bool
Expand All @@ -467,5 +573,102 @@ public static function shouldRecordTagspecValidatedTest(TagSpec $tag_spec, array
(!empty(self::getTagSpecName($tag_spec)) && isset($detail_or_names_to_track[self::getTagSpecName($tag_spec)]));
}

/**
* @param Context $context
* @param array $attrs_by_key
* @param SValidationResult $result
*/
public function validateLayout(Context $context, array $attrs_by_key, SValidationResult $result)
{
assert(!empty($this->spec->amp_layout));

$layout_attr = isset($attrs_by_key['layout']) ? $attrs_by_key['layout'] : null;
$width_attr = isset($attrs_by_key['width']) ? $attrs_by_key['width'] : null;
$height_attr = isset($attrs_by_key['height']) ? $attrs_by_key['height'] : null;
$sizes_attr = isset($attrs_by_key['sizes']) ? $attrs_by_key['sizes'] : null;
$heights_attr = isset($attrs_by_key['heights']) ? $attrs_by_key['heights'] : null;

$input_layout = self::parseLayout($layout_attr);
if (!empty($layout_attr) && $input_layout === AmpLayoutLayout::UNKNOWN) {
$context->addError(ValidationErrorCode::INVALID_ATTR_VALUE,
['layout', self::getTagSpecName($this->spec), $layout_attr], $this->spec->spec_url, $result);
return;
}

$input_width = new CssLengthAndUnit($width_attr, true);
if (!$input_width->is_valid) {
$context->addError(ValidationErrorCode::INVALID_ATTR_VALUE,
['width', self::getTagSpecName($this->spec), $width_attr], $this->spec->spec_url, $result);
return;
}

$input_height = new CssLengthAndUnit($height_attr, true);
if (!$input_height->is_valid) {
$context->addError(ValidationErrorCode::INVALID_ATTR_VALUE,
['height', self::getTagSpecName($this->spec), $height_attr], $this->spec->spec_url, $result);
return;
}

$width = self::calculateWidth($this->spec->amp_layout, $input_layout, $input_width);
$height = self::calculateHeight($this->spec->amp_layout, $input_layout, $input_height);
$layout = self::calculateLayout($input_layout, $width, $height, $sizes_attr, $heights_attr);

if ($height->is_auto && $layout !== AmpLayoutLayout::FLEX_ITEM) {
$context->addError(ValidationErrorCode::INVALID_ATTR_VALUE,
['height', self::getTagSpecName($this->spec), $height_attr], $this->spec->spec_url, $result);
return;
}

if (!in_array($layout, $this->spec->amp_layout->supported_layouts)) {
$code = empty($layout_attr) ? ValidationErrorCode::IMPLIED_LAYOUT_INVALID :
ValidationErrorCode::SPECIFIED_LAYOUT_INVALID;
$context->addError($code,
[$layout, self::getTagSpecName($this->spec)], $this->spec->spec_url, $result);
return;
}

if (in_array($layout, [AmpLayoutLayout::FIXED, AmpLayoutLayout::FIXED_HEIGHT, AmpLayoutLayout::RESPONSIVE])
&& !$height->is_set
) {
$context->addError(ValidationErrorCode::MANDATORY_ATTR_MISSING,
['height', self::getTagSpecName($this->spec)],
$this->spec->spec_url, $result);
return;
}

if ($layout === AmpLayoutLayout::FIXED_HEIGHT && $width->is_set && !$width->is_auto) {
$context->addError(ValidationErrorCode::ATTR_VALUE_REQUIRED_BY_LAYOUT,
[$width_attr, 'width', self::getTagSpecName($this->spec), 'FIXED_HEIGHT', 'auto'],
$this->spec->spec_url, $result);
return;
}

if (in_array($layout, [AmpLayoutLayout::FIXED, AmpLayoutLayout::RESPONSIVE])) {
if (!$width->is_set) {
$context->addError(ValidationErrorCode::MANDATORY_ATTR_MISSING,
['width', self::getTagSpecName($this->spec)], $this->spec->spec_url, $result);
return;
} else if ($width->is_auto) {
$context->addError(ValidationErrorCode::INVALID_ATTR_VALUE,
['width', self::getTagSpecName($this->spec), 'auto'], $this->spec->spec_url, $result);
return;
}
}

if ($layout === AmpLayoutLayout::RESPONSIVE && $width->unit !== $height->unit) {
$context->addError(ValidationErrorCode::INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT,
[self::getTagSpecName($this->spec), $width->unit, $height->unit], $this->spec->spec_url, $result);
return;
}

if (!empty($heights_attr) && $layout !== AmpLayoutLayout::RESPONSIVE) {
$code = empty($layout_attr) ? ValidationErrorCode::ATTR_DISALLOWED_BY_IMPLIED_LAYOUT :
ValidationErrorCode::ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT;
$context->addError($code,
['heights', self::getTagSpecName($this->spec), $layout], $this->spec->spec_url, $result);
return;
}
}

}

Loading

0 comments on commit faf08b6

Please sign in to comment.