Skip to content

Commit

Permalink
implement custom customers for know at-rules and related changes
Browse files Browse the repository at this point in the history
- custom customers for at-rules can be defined via parser.atrule
- move default at-rule expression customer to parser/sequence.js
- accept option for parse() to specify at-rule for atruleExpression
context
- define custom customer for @media expression
- implement MediaQueryList, MediaQuery and Media feature node types
  • Loading branch information
lahmatiy committed Feb 1, 2017
1 parent 616a0b7 commit 53b8125
Show file tree
Hide file tree
Showing 18 changed files with 418 additions and 189 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ Parses CSS to AST.
Options:

- `context` String – parsing context, useful when some part of CSS is parsing (see below)
- `property` String – make sense for `declaration` context to apply some property specific parse rules
- `atrule` String – make sense for `atruleExpression` context to apply some atrule specific parse rules
- `property` String – make sense for `value` context to apply some property specific parse rules
- `positions` Boolean – should AST contains node position or not, store data in `info` property of nodes (`false` by default)
- `filename` String – filename of source that adds to info when `positions` is true, uses for source map generation (`<unknown>` by default)
- `line` Number – initial line number, useful when parse fragment of CSS to compute correct positions
Expand Down
8 changes: 8 additions & 0 deletions lib/parser/atrule/media.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
expression: function() {
return this.MediaQueryList();
},
block: function() {
return this.Block(this.Rule);
}
};
33 changes: 25 additions & 8 deletions lib/parser/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ var getType = require('./nodes/Type');
var getUniversal = require('./nodes/Universal');
var getRaw = require('./nodes/Raw');
var getNumber = require('./nodes/Number');
var getMediaQueryList = require('./nodes/MediaQueryList');
var getMediaQuery = require('./nodes/MediaQuery');
var getMediaFeature = require('./nodes/MediaFeature');

function scanIdent(varAllowed) {
// optional first -
Expand Down Expand Up @@ -93,6 +96,9 @@ Parser.prototype = {
expression: require('./functions/expression'),
var: require('./functions/var')
},
atrule: {
'media': require('./atrule/media')
},
pseudo: {
'lang': sequence.singleIdentifier,
'dir': sequence.singleIdentifier,
Expand Down Expand Up @@ -156,11 +162,15 @@ Parser.prototype = {
Hash: getHash,
Raw: getRaw,
Number: getNumber,
MediaQueryList: getMediaQueryList,
MediaQuery: getMediaQuery,
MediaFeature: getMediaFeature,

scanIdent: scanIdent,
readIdent: readIdent,
readSC: readSC,
readValueSequence: sequence.value,
readAtruleExpression: sequence.atruleExpression,
readValue: sequence.value,

getLocation: function getLocation(start, end) {
if (this.needPositions) {
Expand All @@ -183,14 +193,21 @@ Parser.prototype = {
this.filename = options.filename || '<unknown>';
this.needPositions = Boolean(options.positions);

if (!this.context.hasOwnProperty(context)) {
throw new Error('Unknown context `' + context + '`');
}
switch (context) {
case 'value':
ast = this.Value(options.property ? String(options.property) : null);
break;

case 'atruleExpression':
ast = this.Value(options.atrule ? String(options.atrule) : null);
break;

default:
if (!this.context.hasOwnProperty(context)) {
throw new Error('Unknown context `' + context + '`');
}

if (context === 'value') {
ast = this.Value(options.property ? String(options.property) : null);
} else {
ast = this.context[context].call(this);
ast = this.context[context].call(this);
}

if (!this.scanner.eof) {
Expand Down
5 changes: 3 additions & 2 deletions lib/parser/nodes/Atrule.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var SEMICOLON = TYPE.Semicolon;
var COMMERCIALAT = TYPE.CommercialAt;
var LEFTCURLYBRACKET = TYPE.LeftCurlyBracket;
var RIGHTCURLYBRACKET = TYPE.RightCurlyBracket;
var DISALLOW_VAR = false;

function isBlockAtrule() {
for (var offset = 1, type; type = this.scanner.lookupType(offset); offset++) {
Expand All @@ -28,8 +29,8 @@ module.exports = function Atrule() {

this.scanner.eat(COMMERCIALAT);

name = this.readIdent(false);
expression = this.AtruleExpression();
name = this.readIdent(DISALLOW_VAR);
expression = this.AtruleExpression(name);

switch (this.scanner.tokenType) {
case SEMICOLON:
Expand Down
86 changes: 18 additions & 68 deletions lib/parser/nodes/AtruleExpression.js
Original file line number Diff line number Diff line change
@@ -1,81 +1,31 @@
var List = require('../../utils/list');
var TYPE = require('../../scanner').TYPE;

var WHITESPACE = TYPE.Whitespace;
var STRING = TYPE.String;
var COMMENT = TYPE.Comment;
var LEFTPARENTHESIS = TYPE.LeftParenthesis;
var LEFTCURLYBRACKET = TYPE.LeftCurlyBracket;
var COMMA = TYPE.Comma;
var COLON = TYPE.Colon;
var SEMICOLON = TYPE.Semicolon;

module.exports = function AtruleExpression() {
var start;
var children = null;
var wasSpace = false;
var lastNonSpace = null;
var child;

this.readSC();
start = this.scanner.tokenStart;

scan:
while (!this.scanner.eof) {
switch (this.scanner.tokenType) {
case SEMICOLON:
case LEFTCURLYBRACKET:
break scan;

case WHITESPACE:
wasSpace = true;
this.scanner.next();
continue;

case COMMENT: // ignore comments
this.scanner.next();
continue;

case COMMA:
child = this.Operator();
break;

case COLON:
child = this.Pseudo();
break;

case LEFTPARENTHESIS:
child = this.Parentheses(this.scopeAtruleExpression);
break;

case STRING:
child = this.String();
break;

default:
child = this.Any(this.scopeAtruleExpression);
module.exports = function AtruleExpression(name) {
// custom consumer
if (this.atrule.hasOwnProperty(name)) {
if (typeof this.atrule[name].expression === 'function') {
return this.atrule[name].expression.call(this);
} else {
return null;
}
}

if (children === null) {
children = new List();
}
// default consumer
this.readSC();

if (wasSpace) {
wasSpace = false;
children.appendData(this.SPACE_NODE);
}
var start = this.scanner.tokenStart;
var end = start;
var children = this.readAtruleExpression();

lastNonSpace = this.scanner.tokenStart;
children.appendData(child);
if (children === null || children.isEmpty()) {
return null;
}

if (children === null) {
return null;
if (this.needPositions) {
end = children.last().loc.end.offset;
}

return {
type: 'AtruleExpression',
loc: this.getLocation(start, lastNonSpace !== null ? lastNonSpace : this.scanner.tokenStart),
loc: this.getLocation(start, end),
children: children
};
};
55 changes: 55 additions & 0 deletions lib/parser/nodes/MediaFeature.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
var TYPE = require('../../scanner').TYPE;

var IDENTIFIER = TYPE.Identifier;
var NUMBER = TYPE.Number;
var LEFTPARENTHESIS = TYPE.LeftParenthesis;
var RIGHTPARENTHESIS = TYPE.RightParenthesis;
var COLON = TYPE.Colon;
var DISALLOW_VAR = false;

module.exports = function MediaFeature() {
var start = this.scanner.tokenStart;
var name;
var value = null;

this.scanner.eat(LEFTPARENTHESIS);
this.readSC();

name = this.readIdent(DISALLOW_VAR);
this.readSC();

if (this.scanner.tokenType === COLON) {
this.scanner.next();
this.readSC();

switch (this.scanner.tokenType) {
case NUMBER:
if (this.scanner.lookupType(1) === IDENTIFIER) {
value = this.Dimension();
} else {
value = this.Number();
}

break;

case IDENTIFIER:
value = this.Identifier(DISALLOW_VAR);

break;

default:
this.scanner.error('Number, dimension or identifier is expected');
}

this.readSC();
}

this.scanner.eat(RIGHTPARENTHESIS);

return {
type: 'MediaFeature',
loc: this.getLocation(start, this.scanner.tokenStart),
name: name,
value: value
};
};
63 changes: 63 additions & 0 deletions lib/parser/nodes/MediaQuery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
var List = require('../../utils/list');
var TYPE = require('../../scanner').TYPE;

var IDENTIFIER = TYPE.Identifier;
var LEFTPARENTHESIS = TYPE.LeftParenthesis;
var DISALLOW_VAR = false;

module.exports = function MediaQuery() {
this.readSC();

var start = this.scanner.tokenStart;
var end = start;
var children = new List();

if (this.scanner.lookupValue(0, 'only') ||
this.scanner.lookupValue(0, 'not')) {
// [ not | only ] s+ ident
children.appendData(this.Identifier(DISALLOW_VAR));
this.readSC();

children.appendData(this.Identifier(DISALLOW_VAR));
this.readSC();
} else if (this.scanner.tokenType === IDENTIFIER) {
// ident
children.appendData(this.Identifier(DISALLOW_VAR));
this.readSC();
} else if (this.scanner.tokenType === LEFTPARENTHESIS) {
// <MediaFeature>
children.appendData(this.MediaFeature());
this.readSC();
} else {
this.scanner.error('Identifier or open parenthesis is expected');
}

while (!this.scanner.eof) {
if (this.scanner.lookupValue(0, 'and')) {
if (children.isEmpty()) {
this.scanner.error('Unexpected `and` keyword');
}

children.appendData(this.Identifier(DISALLOW_VAR));
this.readSC();
} else {
if (this.scanner.tokenType === LEFTPARENTHESIS) {
this.scanner.error('`and` keyword is expected');
}
break;
}

children.appendData(this.MediaFeature());
this.readSC();
}

if (this.needPositions) {
end = children.last().loc.end.offset;
}

return {
type: 'MediaQuery',
loc: this.getLocation(start, end),
children: children
};
};
33 changes: 33 additions & 0 deletions lib/parser/nodes/MediaQueryList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
var List = require('../../utils/list');
var COMMA = require('../../scanner').TYPE.Comma;

module.exports = function MediaQueryList(relative) {
this.readSC();

var start = this.scanner.tokenStart;
var end = start;
var children = new List();
var mediaQuery = null;

while (!this.scanner.eof) {
mediaQuery = this.MediaQuery(relative);
children.appendData(mediaQuery);

if (this.needPositions) {
end = mediaQuery.children.last().loc.end.offset;
}

if (this.scanner.tokenType === COMMA) {
this.scanner.next();
continue;
}

break;
}

return {
type: 'MediaQueryList',
loc: this.getLocation(start, end),
children: children
};
};
2 changes: 1 addition & 1 deletion lib/parser/nodes/Value.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ module.exports = function Value(property) {
}

var start = this.scanner.tokenStart;
var children = this.readValueSequence(this.scopeValue);
var children = this.readValue(this.scopeValue);

return {
type: 'Value',
Expand Down
Loading

0 comments on commit 53b8125

Please sign in to comment.