Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create amp-validator-rules package #377

Merged
merged 13 commits into from
Jul 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ A collection of AMP tools making it easier to publish and host AMP pages. The fo
- **[amp-runtime-version](./packages/runtime-version):** a javascript library for querying the current AMP runtime version.
- **[amp-update-cache](./packages/update-cache):** a javascript library for updating AMP documents in AMP Caches.
- **[amp-update-linter](./packages/linter):** a javascript library for linting AMP documents (includes CLI mode).
- **[amp-validator-rules](./packages/validator-rules):** a javascript library for querying AMP validator rules.

## Development

Expand All @@ -43,7 +44,7 @@ git clone https://github.com/your-fork/amp-toolbox.git
# step into local repo
cd amp-toolbox

# install dependencies
# install dependencies
npm install

# run tests
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"@ampproject/toolbox-optimizer": "file:packages/optimizer",
"@ampproject/toolbox-optimizer-express": "file:packages/optimizer-express",
"@ampproject/toolbox-runtime-version": "file:packages/runtime-version",
"@ampproject/toolbox-update-cache": "file:packages/update-cache"
"@ampproject/toolbox-update-cache": "file:packages/update-cache",
"@ampproject/toolbox-validator-rules": "file:packages/validator-rules"
}
}
1 change: 1 addition & 0 deletions packages/validator-rules/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
validator.json
89 changes: 89 additions & 0 deletions packages/validator-rules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# AMP-Toolbox Validator Rules

Queries published AMP Validator rules and extracts information about required
markup and attributes for all AMP formats.

## Usage

Install via:

```
$ npm install @ampproject/toolbox-validator-rules@canary
```

### Including the Module

#### ES Module (Browser)

```javascript
import validatorRules from '@ampproject/toolbox-validator-rules';
```

#### CommonJs (Node)

```javascript
const validatorRules = require('@ampproject/toolbox-validator-rules');
```

### Using the module

```javascript
// Loads the validator rules remotely with default options
const rules = await validatorRules.fetch();


// The raw unprocessed rules
console.log(rules.raw);

// All tags, combined with their respective attribute lists
console.log(rules.tags);

// All extensions
console.log(rules.extensions);

// Get all tag names used in AMP for Email
// The supported formats are AMP, AMP4EMAIL, AMP4ADS and ACTIONS
const tags = rules.getTagsForFormat('AMP4EMAIL');

// Display their names
console.log(tags.map(tag => tag.tagName));

// Get information about an extension
const ext = rules.getExtension('AMP4EMAIL', 'amp-carousel');

// Display supported versions
console.log(ext.versions);
```

### Format of rules

The rules used closely follow the proto definitions from [validator.proto](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto).

Specifically:

- The `raw` property is unprocessed [ValidatorRules](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto#L643), the same format used by `https://cdn.ampproject.org/v0/validator.json`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: turn this into a link

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think that makes sense, clicking that link is probably not very useful, it's more meaningful as an identifier.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd want to click it :-). But I agree unformatted JSON is not helpful.

- The result of `getTagsForFormat` and the `tags` property is a list of [TagSpec](https://github.com/ampproject/amphtml/blob/b892d81467594cab5473c803e071af5108f834a6/validator/validator.proto#L463)
- The result of `getExtension` is [ExtensionSpec](https://github.com/ampproject/amphtml/blob/b892d81467594cab5473c803e071af5108f834a6/validator/validator.proto#L388) with the `htmlFormat` field from `TagSpec`
- The `extensions` property a list of [ExtensionSpec](https://github.com/ampproject/amphtml/blob/b892d81467594cab5473c803e071af5108f834a6/validator/validator.proto#L388) with the `htmlFormat` field from `TagSpec`
- The `errors` property combines [ErrorFormat](https://github.com/ampproject/amphtml/blob/b892d81467594cab5473c803e071af5108f834a6/validator/validator.proto#L874) and [ErrorSpecificity](https://github.com/ampproject/amphtml/blob/b892d81467594cab5473c803e071af5108f834a6/validator/validator.proto#L869)

### Options

`fetch` optionally accepts an options object allowing you to customize its
behaviour.

The following options are supported:

* `noCache`: true to always fetch latest rules (by default, subsequent calls to `fetch` reuse the same result).
* `rules`: object to use locally specified rules instead of fetching them from the AMP CDN.
* `url`: override the URL where validator rules are fetched from.
* `source`: one of `'local'` (load rules from local file named "validator.json"), `'remote'` (fetch rules from CDN) or `'auto'` which is the default (tries looking for the local file first, then tries to fetch from CDN).

Example:

```
validatorRules.fetch({
noCache: true,
source: 'remote'
});
```
39 changes: 39 additions & 0 deletions packages/validator-rules/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright 2019 The AMP HTML Authors. All Rights Reserved.
*
* 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.
*/

'use strict';

const loadRules = require('./lib/loadRules');
const AmpValidatorRules = require('./lib/AmpValidatorRules');

let cached = null;

async function fetch(opt = {}) {
if (!opt.noCache && cached) {
return cached;
}

let rules = opt.rules;

if (!rules) {
rules = await loadRules(opt);
}

cached = new AmpValidatorRules(rules);
return cached;
}

module.exports = {fetch};
201 changes: 201 additions & 0 deletions packages/validator-rules/lib/AmpValidatorRules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Copyright 2019 The AMP HTML Authors. All Rights Reserved.
*
* 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.
*/

class AmpValidatorRules {
/**
* Creates an instance of AmpValidatorRules.
* @param {Object} rules - rules imported from validator.json
*/
constructor(rules) {
/**
* Unprocessed validator rules.
* @type {Object}
*/
this.raw = rules;
/**
* List of all the tags processed from rules.
* @type {Array<Object>}
*/
this.tags = [];
/**
* List of all the extensions processed from rules.
* @type {Array<Object>}
*/
this.extensions = [];
/**
* Map of errors and their associated format and specificity.
* @type {Object}
*/
this.errors = {};

this.extensionCache_ = {};
this.initRules_(rules);
}

/**
* Returns the list of supported tags for the given format.
*
* @param {string} format - Format to return tags for
* @param {boolean} [transformed] - Use transformed version of the format
* @return {Array<Object>} List of tags supported by the given format
*/
getTagsForFormat(format, transformed = false) {
format = format.toLowerCase();
return this.tags
.filter(
(tag) =>
tag.htmlFormat.includes(format.toUpperCase()) &&
this.checkEntityFormat_(tag, format) &&
this.checkEntityTransformed_(tag, transformed)
)
.map((tag) => {
tag = Object.assign({}, tag);
tag.attrs = tag.attrs.filter(
(attr) =>
this.checkEntityFormat_(attr, format) &&
this.checkEntityTransformed_(attr, transformed)
);
return tag;
});
}

/**
* Returns the AMP extension spec for the given format and name.
*
* @param {string} format - Format to filter on
* @param {string} extension - Extension name
* @return {Object} Extension spec
*/
getExtension(format, extension) {
format = format.toLowerCase();
extension = extension.toLowerCase();
const key = `${format}|${extension}`;
return this.extensionCache_[key] || null;
}

checkEntityTransformed_(entity, transformed) {
const isEnabled = this.isEnabled_(entity, 'transformed');
const isDisabled = this.isDisabled_(entity, 'transformed');
if (transformed) {
return isEnabled !== false && isDisabled !== true;
}
return isEnabled !== true && isDisabled !== false;
}

checkEntityFormat_(entity, format) {
format = format.toLowerCase();
const isEnabled = this.isEnabled_(entity, format);
const isDisabled = this.isDisabled_(entity, format);
return isEnabled !== false && isDisabled !== true;
}

isEnabled_(entity, format) {
if (!entity.enabledBy) {
return null;
}
return entity.enabledBy.includes(format);
}

isDisabled_(entity, format) {
if (!entity.disabledBy) {
return null;
}
return entity.disabledBy.includes(format);
}

initRules_(rules) {
this.initErrors_(rules);
this.initAttrLists_(rules);
this.initTags_(rules);
this.initExtensions_(rules);
}

initErrors_(rules) {
this.errors = {};
for (const errorFormat of rules.errorFormats) {
const error = this.errors[errorFormat.code] || {};
error.format = errorFormat.format;
this.errors[errorFormat.code] = error;
}
for (const errorSpecificity of rules.errorSpecificity) {
const error = this.errors[errorSpecificity.code] || {};
error.specificity = errorSpecificity.specificity;
this.errors[errorSpecificity.code] = error;
}
}

initAttrLists_(rules) {
this.attrLists_ = {};
this.specialAttrLists_ = {};
for (const {name, attrs} of rules.attrLists) {
if (name.startsWith('$')) {
this.specialAttrLists_[name] = attrs;
} else {
this.attrLists_[name] = attrs;
}
}
this.specialAttrLists_.$AMP_LAYOUT_ATTRS.forEach(
(attr) => (attr.layout = true)
);
this.specialAttrLists_.$GLOBAL_ATTRS.forEach((attr) => (attr.global = true));
}

initTags_(rules) {
this.tags = rules.tags
.filter((tag) => !tag.extensionSpec)
.map((tag) => {
tag.attrs = tag.attrs || [];

// `attrLists` contains list IDs that are looked up from the global
// attribute lists and merged into `attrs`.
if (tag.attrLists) {
for (const attrList of tag.attrLists) {
tag.attrs.push(...this.attrLists_[attrList]);
}
delete tag.attrLists;

This comment was marked as resolved.

}

// $AMP_LAYOUT_ATTRS are present in all components with ampLayout
if (tag.ampLayout) {
tag.attrs.push(...this.specialAttrLists_.$AMP_LAYOUT_ATTRS);
}

// $GLOBAL_ATTRS are present in all components
tag.attrs.push(...this.specialAttrLists_.$GLOBAL_ATTRS);

return tag;
});
}

initExtensions_(rules) {
this.extensions = rules.tags
.filter((tag) => tag.extensionSpec)
.map((tag) =>
Object.assign({}, tag.extensionSpec, {htmlFormat: tag.htmlFormat})
);

for (const extension of this.extensions) {
const name = extension.name.toLowerCase();
for (let format of extension.htmlFormat) {
format = format.toLowerCase();
const key = `${format}|${name}`;
this.extensionCache_[key] = extension;
}
}
}
}

module.exports = AmpValidatorRules;
Loading