Skip to content

Commit

Permalink
Add full support for parsing attribute selectors.
Browse files Browse the repository at this point in the history
  • Loading branch information
powdercloud authored and Gregable committed Mar 26, 2016
1 parent eddc6fd commit eba6adf
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 33 deletions.
132 changes: 121 additions & 11 deletions validator/css-selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ goog.require('parse_css.EOFToken');
goog.require('parse_css.ErrorToken');
goog.require('parse_css.Token');
goog.require('parse_css.TokenStream');
goog.require('parse_css.extractASimpleBlock');

/**
* Abstract super class for CSS Selectors. The Token class, which this
Expand Down Expand Up @@ -269,17 +268,24 @@ parse_css.parseAnIdSelector = function(tokenStream) {
/**
* An attribute selector matches document nodes based on their attributes.
* http://www.w3.org/TR/css3-selectors/#attribute-selectors
* Note: this is a placeholder implementation which has the raw tokens
* as its value. We'll refine this in the future.
*
* Typically written as '[foo=bar]'.
*/
parse_css.AttrSelector = class extends parse_css.Selector {
/**
* @param {!Array<!parse_css.Token>} value
* @param {string?} namespacePrefix
* @param {!string} attrName
* @param {string?} matchOperator
* @param {string?} value
*/
constructor(value) {
/** @type {!Array<!parse_css.Token>} */
constructor(namespacePrefix, attrName, matchOperator, value) {
/** @type {string?} */
this.namespacePrefix = namespacePrefix;
/** @type {!string} */
this.attrName = attrName;
/** @type {string?} */
this.matchOperator = matchOperator;
/** @type {string?} */
this.value = value;
/** @type {parse_css.TokenType} */
this.tokenType = parse_css.TokenType.ATTR_SELECTOR;
Expand All @@ -288,7 +294,10 @@ parse_css.AttrSelector = class extends parse_css.Selector {
/** @inheritDoc */
toJSON() {
const json = super.toJSON();
json['value'] = recursiveArrayToJSON(this.value);
json['namespacePrefix'] = this.namespacePrefix;
json['attrName'] = this.attrName;
json['matchOperator'] = this.matchOperator;
json['value'] = this.value;
return json;
}

Expand All @@ -301,16 +310,112 @@ parse_css.AttrSelector = class extends parse_css.Selector {
/**
* tokenStream.current() must be the hash token.
* @param {!parse_css.TokenStream} tokenStream
* @return {!parse_css.AttrSelector}
* @return {!parse_css.AttrSelector|!parse_css.ErrorToken}
*/
parse_css.parseAnAttrSelector = function(tokenStream) {
goog.asserts.assert(
tokenStream.current() instanceof parse_css.OpenSquareToken,
'Precondition violated: must be an OpenSquareToken');
const start = tokenStream.current();
const block = parse_css.extractASimpleBlock(tokenStream);
tokenStream.consume(); // Consumes '['.
// This part is defined in https://www.w3.org/TR/css3-selectors/#attrnmsp:
// Attribute selectors and namespaces. It is similar to parseATypeSelector.
let namespacePrefix = null;
if (isDelim(tokenStream.current(), '|')) {
namespacePrefix = '';
tokenStream.consume();
} else if (isDelim(tokenStream.current(), '*') &&
isDelim(tokenStream.next(), '|')) {
namespacePrefix = '*';
tokenStream.consume();
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.IdentToken &&
isDelim(tokenStream.next(), '|')) {
const ident = goog.asserts.assertInstanceof(
tokenStream.current(), parse_css.IdentToken);
namespacePrefix = ident.value;
tokenStream.consume();
tokenStream.consume();
}
// Now parse the attribute name. This part is mandatory.
if (!(tokenStream.current() instanceof parse_css.IdentToken)) {
const error = new parse_css.ErrorToken(
amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_ATTR_SELECTOR,
['style']);
error.line = start.line;
error.col = start.col;
return error;
}
const ident = goog.asserts.assertInstanceof(
tokenStream.current(), parse_css.IdentToken);
const attrName = ident.value;
tokenStream.consume();

// After the attribute name, we may see an operator; if we do, then
// we must see either a string or an identifier. This covers
// 6.3.1 Attribute presence and value selectors
// (https://www.w3.org/TR/css3-selectors/#attribute-representation) and
// 6.3.2 Substring matching attribute selectors
// (https://www.w3.org/TR/css3-selectors/#attribute-substrings).

/** @type {string?} */
let matchOperator = null;
if (tokenStream.current() instanceof parse_css.DelimToken &&
/** @type {!parse_css.DelimToken} */(
tokenStream.current()).value === '=') {
matchOperator = '=';
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.IncludeMatchToken) {
matchOperator = '~=';
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.DashMatchToken) {
matchOperator = '|=';
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.PrefixMatchToken) {
matchOperator = '^=';
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.SuffixMatchToken) {
matchOperator = '$=';
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.SubstringMatchToken) {
matchOperator = '*=';
tokenStream.consume();
}
/** @type {string?} */
let value = null;
if (matchOperator !== null) { // If we saw an operator, parse the value.
if (tokenStream.current() instanceof parse_css.IdentToken) {
const ident = goog.asserts.assertInstanceof(
tokenStream.current(), parse_css.IdentToken);
value = ident.value;
tokenStream.consume();
} else if (tokenStream.current() instanceof parse_css.StringToken) {
const str = goog.asserts.assertInstanceof(
tokenStream.current(), parse_css.StringToken);
value = str.value;
tokenStream.consume();
} else {
const error = new parse_css.ErrorToken(
amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_ATTR_SELECTOR,
['style']);
error.line = start.line;
error.col = start.col;
return error;
}
}
// The attribute selector must in any case terminate with a close square
// token.
if (!(tokenStream.current() instanceof parse_css.CloseSquareToken)) {
const error = new parse_css.ErrorToken(
amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_ATTR_SELECTOR,
['style']);
error.line = start.line;
error.col = start.col;
return error;
}
tokenStream.consume();
const selector = new parse_css.AttrSelector(block);
const selector = new parse_css.AttrSelector(
namespacePrefix, attrName, matchOperator, value);
selector.line = start.line;
selector.col = start.col;
return selector;
Expand Down Expand Up @@ -525,6 +630,7 @@ parse_css.parseASimpleSelectorSequence = function(tokenStream) {
tokenStream.current() instanceof parse_css.IdentToken) {
typeSelector = parse_css.parseATypeSelector(tokenStream);
}
/** @type {!Array<!parse_css.Selector>} */
const otherSelectors = [];
while (true) {
if (tokenStream.current() instanceof parse_css.HashToken) {
Expand All @@ -533,7 +639,11 @@ parse_css.parseASimpleSelectorSequence = function(tokenStream) {
tokenStream.next() instanceof parse_css.IdentToken) {
otherSelectors.push(parse_css.parseAClassSelector(tokenStream));
} else if (tokenStream.current() instanceof parse_css.OpenSquareToken) {
otherSelectors.push(parse_css.parseAnAttrSelector(tokenStream));
const attrSelector = parse_css.parseAnAttrSelector(tokenStream);
if (attrSelector instanceof parse_css.ErrorToken) {
return attrSelector;
}
otherSelectors.push(attrSelector);
} else if (tokenStream.current() instanceof parse_css.ColonToken) {
const pseudo = parse_css.parseAPseudoSelector(tokenStream);
if (pseudo instanceof parse_css.ErrorToken) {
Expand Down
30 changes: 17 additions & 13 deletions validator/parse-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,19 +452,23 @@ class Canonicalizer {
const contents = parse_css.extractASimpleBlock(tokenStream);

switch (this.blockTypeFor(rule)) {
case parse_css.BlockType.PARSE_AS_RULES:
rule.rules = this.parseAListOfRules(
contents, /* topLevel */ false, errors);
break;
case parse_css.BlockType.PARSE_AS_DECLARATIONS:
rule.declarations = this.parseAListOfDeclarations(contents, errors);
break;
case parse_css.BlockType.PARSE_AS_IGNORE:
break;
default:
goog.asserts.fail(
'Unrecognized blockType ' + this.blockTypeFor(rule));
break;
case parse_css.BlockType.PARSE_AS_RULES: {
rule.rules = this.parseAListOfRules(
contents, /* topLevel */ false, errors);
break;
}
case parse_css.BlockType.PARSE_AS_DECLARATIONS: {
rule.declarations = this.parseAListOfDeclarations(contents, errors);
break;
}
case parse_css.BlockType.PARSE_AS_IGNORE: {
break;
}
default: {
goog.asserts.fail(
'Unrecognized blockType ' + this.blockTypeFor(rule));
break;
}
}
return rule;
}
Expand Down
47 changes: 40 additions & 7 deletions validator/parse-css_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1049,18 +1049,51 @@ describe('css_selectors', () => {
assertJSONEquals(
{'line': 1, 'col': 0, 'tokenType': 'SIMPLE_SELECTOR_SEQUENCE',
'otherSelectors':
[{'line': 1, 'col': 1, 'value':
[{'line': 1, 'col': 2, 'tokenType': 'IDENT', 'value': 'href'},
{'line': 1, 'col': 6, 'tokenType': 'DELIM', 'value': '='},
{'line': 1, 'col': 7, 'tokenType': 'STRING', 'value':
'http://www.w3.org/'},
{'line': 1, 'col': 27, 'tokenType': 'EOF_TOKEN'}], 'tokenType':
'ATTR_SELECTOR'}], 'typeSelector':
[{'line': 1, 'col': 1, 'tokenType': 'ATTR_SELECTOR', 'value':
'http://www.w3.org/', 'attrName': 'href', 'matchOperator':
'=', 'namespacePrefix': null}],
'typeSelector':
{'line': 1, 'col': 0, 'elementName': 'a', 'namespacePrefix':
null, 'tokenType': 'TYPE_SELECTOR'}},
selector);
});

it('parses a selectors group with more attrib matches', () => {
const tokens = parseSelectorForTest(
'elem[attr1="v1"][attr2=value2]' +
'[attr3~="foo"][attr4|="bar"][attr5|="baz"][attr6$=boo][attr7*=bang]');
const tokenStream = new parse_css.TokenStream(tokens);
tokenStream.consume();
const selector = parse_css.parseASelectorsGroup(tokenStream);
assertJSONEquals(
{'line': 1, 'col': 0, 'tokenType': 'SIMPLE_SELECTOR_SEQUENCE',
'otherSelectors':
[{'line': 1, 'col': 4, 'tokenType': 'ATTR_SELECTOR', 'value':
'v1', 'attrName': 'attr1', 'matchOperator': '=', 'namespacePrefix':
null},
{'line': 1, 'col': 16, 'tokenType': 'ATTR_SELECTOR', 'value':
'value2', 'attrName': 'attr2', 'matchOperator': '=',
'namespacePrefix': null},
{'line': 1, 'col': 30, 'tokenType': 'ATTR_SELECTOR', 'value':
'foo', 'attrName': 'attr3', 'matchOperator': '~=', 'namespacePrefix':
null},
{'line': 1, 'col': 44, 'tokenType': 'ATTR_SELECTOR', 'value':
'bar', 'attrName': 'attr4', 'matchOperator': '|=', 'namespacePrefix':
null},
{'line': 1, 'col': 58, 'tokenType': 'ATTR_SELECTOR', 'value':
'baz', 'attrName': 'attr5', 'matchOperator': '|=', 'namespacePrefix':
null},
{'line': 1, 'col': 72, 'tokenType': 'ATTR_SELECTOR', 'value':
'boo', 'attrName': 'attr6', 'matchOperator': '$=', 'namespacePrefix':
null},
{'line': 1, 'col': 84, 'tokenType': 'ATTR_SELECTOR', 'value':
'bang', 'attrName': 'attr7', 'matchOperator': '*=',
'namespacePrefix': null}], 'typeSelector':
{'line': 1, 'col': 0, 'tokenType': 'TYPE_SELECTOR', 'elementName':
'elem', 'namespacePrefix': null}},
selector);
});

it('parses a selectors group with a pseudo class', () => {
const tokens = parseSelectorForTest('a::b:lang(fr-be)]');
assertJSONEquals(
Expand Down
2 changes: 2 additions & 0 deletions validator/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ function specificity(code) {
return 53;
case amp.validator.ValidationError.Code.DISALLOWED_FIRST_CHILD_TAG_NAME:
return 54;
case amp.validator.ValidationError.Code.CSS_SYNTAX_INVALID_ATTR_SELECTOR:
return 55;
case amp.validator.ValidationError.Code.GENERAL_DISALLOWED_TAG:
return 100;
case amp.validator.ValidationError.Code.DEPRECATED_ATTR:
Expand Down
3 changes: 2 additions & 1 deletion validator/validator.proto
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ message ValidationError {
DEV_WARNING = 3; // DEPRECATED DO NOT USE.
}
optional Severity severity = 6 [ default = ERROR ];
// NEXT AVAILABLE TAG: 59
// NEXT AVAILABLE TAG: 60
enum Code {
UNKNOWN_CODE = 0;
MANDATORY_TAG_MISSING = 1;
Expand Down Expand Up @@ -438,6 +438,7 @@ message ValidationError {
CSS_SYNTAX_INVALID_URL = 53;
CSS_SYNTAX_INVALID_URL_PROTOCOL = 54;
CSS_SYNTAX_DISALLOWED_RELATIVE_URL = 55;
CSS_SYNTAX_INVALID_ATTR_SELECTOR = 59;
}
optional Code code = 1;
optional int32 line = 2 [ default = 1 ];
Expand Down
6 changes: 5 additions & 1 deletion validator/validator.protoascii
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ min_validator_revision_required: 112
# newer versions of the spec file. This is currently a Google internal
# mechanism, validator.js does not use this facility. However, any
# change to this file requires updating this revision id.
spec_file_revision: 189
spec_file_revision: 190
# Rules for AMP HTML
# (https://www.ampproject.org/docs/reference/spec.html)
#
Expand Down Expand Up @@ -4707,3 +4707,7 @@ error_formats {
code: CSS_SYNTAX_DISALLOWED_RELATIVE_URL
format: "CSS syntax error in tag '%1' - disallowed relative url '%2'."
}
error_formats {
code: CSS_SYNTAX_INVALID_ATTR_SELECTOR
format: "CSS syntax error in tag '%1' - invalid attribute selector."
}

0 comments on commit eba6adf

Please sign in to comment.