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

Validator Roll-Up #2595

Merged
merged 12 commits into from
Mar 15, 2016
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
70 changes: 57 additions & 13 deletions validator/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ def CheckPrereqs():
'Please feel free to edit the source and fix it to your needs.')

# Ensure source files are available.
for f in ['validator.protoascii', 'validator.proto', 'validator_gen.py',
for f in ['validator.protoascii', 'validator.proto', 'validator_gen_js.py',
'package.json', 'validator.js', 'validator_test.js',
'validator-in-browser.js', 'tokenize-css.js', 'parse-css.js']:
'validator-in-browser.js', 'tokenize-css.js', 'parse-css.js',
'parse-srcset.js']:
if not os.path.exists(f):
Die('%s not found. Must run in amp_validator source directory.' % f)

Expand Down Expand Up @@ -145,7 +146,7 @@ def GenValidatorPb2Py(out_dir):


def GenValidatorGeneratedJs(out_dir):
"""Calls validator_gen to generate validator-generated.js.
"""Calls validator_gen_js to generate validator-generated.js.

Args:
out_dir: directory name of the output directory. Must not have slashes,
Expand All @@ -160,20 +161,50 @@ def GenValidatorGeneratedJs(out_dir):
from google.protobuf import text_format
from google.protobuf import descriptor
from dist import validator_pb2
import validator_gen
import validator_gen_js
out = []
validator_gen.GenerateValidatorGeneratedJs(specfile='validator.protoascii',
validator_pb2=validator_pb2,
text_format=text_format,
descriptor=descriptor,
out=out)
validator_gen_js.GenerateValidatorGeneratedJs(
specfile='validator.protoascii',
validator_pb2=validator_pb2,
text_format=text_format,
descriptor=descriptor,
out=out)
out.append('')
f = open('%s/validator-generated.js' % out_dir, 'w')
f.write('\n'.join(out))
f.close()
logging.info('... done')


def GenValidatorGeneratedMd(out_dir):
"""Calls validator_gen_md to generate validator-generated.md.

Args:
out_dir: directory name of the output directory. Must not have slashes,
dots, etc.
"""
logging.info('entering ...')
assert re.match(r'^[a-zA-Z_\-0-9]+$', out_dir), 'bad out_dir: %s' % out_dir

# These imports happen late, within this method because they don't necessarily
# exist when the module starts running, and the ones that probably do
# are checked by CheckPrereqs.
from google.protobuf import text_format
from dist import validator_pb2
import validator_gen_md
out = []
validator_gen_md.GenerateValidatorGeneratedMd(
specfile='validator.protoascii',
validator_pb2=validator_pb2,
text_format=text_format,
out=out)
out.append('')
f = open('%s/validator-generated.md' % out_dir, 'w')
f.write('\n'.join(out))
f.close()
logging.info('... done')


def CompileWithClosure(js_files, closure_entry_points, output_file):
"""Compiles the arguments with the Closure compiler for transpilation to ES5.

Expand Down Expand Up @@ -204,8 +235,8 @@ def CompileValidatorMinified(out_dir):
"""
logging.info('entering ...')
CompileWithClosure(
js_files=['htmlparser.js', 'parse-css.js', 'tokenize-css.js',
'%s/validator-generated.js' % out_dir,
js_files=['htmlparser.js', 'parse-css.js', 'parse-srcset.js',
'tokenize-css.js', '%s/validator-generated.js' % out_dir,
'validator-in-browser.js', 'validator.js'],
closure_entry_points=['amp.validator.validateString',
'amp.validator.renderValidationResult',
Expand Down Expand Up @@ -336,8 +367,8 @@ def RunSmokeTest(out_dir, nodejs_cmd):
def CompileValidatorTestMinified(out_dir):
logging.info('entering ...')
CompileWithClosure(
js_files=['htmlparser.js', 'parse-css.js', 'tokenize-css.js',
'%s/validator-generated.js' % out_dir,
js_files=['htmlparser.js', 'parse-css.js', 'parse-srcset.js',
'tokenize-css.js', '%s/validator-generated.js' % out_dir,
'validator-in-browser.js', 'validator.js', 'validator_test.js'],
closure_entry_points=['amp.validator.ValidatorTest'],
output_file='%s/validator_test_minified.js' % out_dir)
Expand All @@ -364,6 +395,16 @@ def CompileParseCssTestMinified(out_dir):
logging.info('... success')


def CompileParseSrcsetTestMinified(out_dir):
logging.info('entering ...')
CompileWithClosure(
js_files=['parse-srcset.js', 'json-testutil.js', 'parse-srcset_test.js',
'%s/validator-generated.js' % out_dir],
closure_entry_points=['parse_srcset.ParseSrcsetTest'],
output_file='%s/parse-srcset_test_minified.js' % out_dir)
logging.info('... success')


def GenerateTestRunner(out_dir):
"""Generates a test runner: a nodejs script that runs our minified tests."""
logging.info('entering ...')
Expand All @@ -378,6 +419,7 @@ def GenerateTestRunner(out_dir):
require('./validator_test_minified');
require('./htmlparser_test_minified');
require('./parse-css_test_minified');
require('./parse-srcset_test_minified');
jasmine.onComplete(function (passed) {
process.exit(passed ? 0 : 1);
});
Expand All @@ -403,12 +445,14 @@ def Main():
SetupOutDir(out_dir='dist')
GenValidatorPb2Py(out_dir='dist')
GenValidatorGeneratedJs(out_dir='dist')
GenValidatorGeneratedMd(out_dir='dist')
CompileValidatorMinified(out_dir='dist')
GenerateValidateBin(out_dir='dist', nodejs_cmd=nodejs_cmd)
RunSmokeTest(out_dir='dist', nodejs_cmd=nodejs_cmd)
CompileValidatorTestMinified(out_dir='dist')
CompileHtmlparserTestMinified(out_dir='dist')
CompileParseCssTestMinified(out_dir='dist')
CompileParseSrcsetTestMinified(out_dir='dist')
GenerateTestRunner(out_dir='dist')
RunTests(out_dir='dist', nodejs_cmd=nodejs_cmd)

Expand Down
169 changes: 169 additions & 0 deletions validator/parse-srcset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* @license
* Copyright 2015 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.
*
* Credits:
* Original version of this file was derived from
* https://github.com/ampproject/amphtml/blob/master/src/srcset.js
*/
goog.provide('parse_srcset.Srcset');
goog.provide('parse_srcset.SrcsetSourceDef');
goog.provide('parse_srcset.parseSrcset');

/**
* A single source within a srcset. Only one: width or DPR can be specified at
* a time.
* @typedef {{
* url: string,
* width: (number|undefined),
* dpr: (number|undefined)
* }}
*/
parse_srcset.SrcsetSourceDef;

/**
* Parses the text representation of srcset into Srcset object.
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes.
* See http://www.w3.org/html/wg/drafts/html/master/semantics.html#attr-img-srcset.
* @param {string} srcset
* @return {!parse_srcset.Srcset}
* @export
*/
parse_srcset.parseSrcset = function(srcset) {
// General grammar: (URL [NUM[w|x]],)*
// Example 1: "image1.png 100w, image2.png 50w"
// Example 2: "image1.png 2x, image2.png"
// Example 3: "image1,100w.png 100w, image2.png 50w"
const sSources = srcset.match(
/\s*([^\s]*)(\s+(-?(\d+(\.(\d+)?)?|\.\d+)[a-zA-Z]))?(\s*,)?/g);
// srcset has to have at least one source
if (sSources.length == 0) return new parse_srcset.Srcset([]);
const sources = [];
sSources.forEach(sSource => {
sSource = sSource.trim();
if (sSource.substr(-1) == ',') {
sSource = sSource.substr(0, sSource.length - 1).trim();
}
const parts = sSource.split(/\s+/, 2);
if (parts.length == 0 ||
parts.length == 1 && !parts[0] ||
parts.length == 2 && !parts[0] && !parts[1]) {
return;
}
const url = parts[0].trim();
if (parts.length == 1 || parts.length == 2 && !parts[1]) {
// If no "w" or "x" specified, we assume it's "1x".
sources.push({url: url, dpr: 1});
} else {
const spec = parts[1].trim().toLowerCase();
const lastChar = spec.substring(spec.length - 1);
if (lastChar == 'w') {
sources.push({url: url, width: parseFloat(spec)});
} else if (lastChar == 'x') {
sources.push({url: url, dpr: parseFloat(spec)});
}
}
});
return new parse_srcset.Srcset(sources);
};


/**
* A srcset object contains one or more sources.
*
* There are two types of sources: width-based and DPR-based. Only one type
* of sources allowed to be specified within a single srcset. Depending on a
* usecase, the components are free to choose any source that best corresponds
* to the required rendering quality and network and CPU conditions. See
* "select" method for details on how this selection is performed.
*
* See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#Attributes
*/
parse_srcset.Srcset = class {
/**
* @param {!Array<!parse_srcset.SrcsetSourceDef>} sources
*/
constructor(sources) {
this.is_successful_ = true;
// Srcset must have at least one source
if (sources.length == 0 ) {
this.is_successful_ = false;
return;
}
/** @private @const {!Array<!parse_srcset.SrcsetSourceDef>} */
this.sources_ = sources;

// Only one type of source specified can be used - width or DPR.
let hasWidth = false;
let hasDpr = false;
this.sources_.forEach(source => {
// Either dpr or width must be specified
if ((source.width && source.dpr) || (!source.width && !source.dpr)) {
this.is_successful_ = false;
}
hasWidth = hasWidth || !!source.width;
hasDpr = hasDpr || !!source.dpr;
});
if (!this.is_successful_) return;

// Srcset cannot have both width and dpr sources
if (hasWidth && hasDpr) {
this.is_successful_ = false;
return;
}

// Source and assert duplicates.
let hasDuplicate = false;
if (hasWidth) {
this.sources_.sort((s1, s2) => {
// Duplicate width
if (s1.width == s2.width) hasDuplicate = true;
return s2.width - s1.width;
});
} else {
this.sources_.sort((s1, s2) => {
// Duplicate dpr
if (s1.dpr == s2.dpr) hasDuplicate = true;
return s2.dpr - s1.dpr;
});
}
if (hasDuplicate) {
this.is_succesful_ = false;
return;
}

/** @private @const {boolean} */
this.widthBased_ = hasWidth;

/** @private @const {boolean} */
this.dprBased_ = hasDpr;
}

/**
* Returns whether parsing sources was successful.
* @return {!boolean}
*/
isSuccessful() {
return this.is_successful_;
}

/**
* Returns all sources in the srcset.
* @return {!Array<!parse_srcset.SrcsetSourceDef>}
*/
getSources() {
return this.sources_;
}
}