Comparing version 1.0.0-alpha13 to 1.0.0-alpha14
@@ -0,1 +1,35 @@ | ||
## 1.0.0-alpha14 (February 3, 2017) | ||
- Implemented `DeclarationList`, `MediaQueryList`, `MediaQuery`, `MediaFeature` and `Ratio` node types | ||
- Implemented `declarationList` context (useful to parse HTML `style` attribute content) | ||
- Implemented custom consumers for `@import`, `@media`, `@page` and `@supports` at-rules | ||
- Implemented `atrule` option for `parse()` config, is used for `atruleExpession` context to specify custom consumer for at-rule if any | ||
- Added `Scanner#skipWS()`, `Scanner#eatNonWS()`, `Scanner#consume()` and `Scanner#consumeNonWS()` helper methods | ||
- Added custom consumers for known functional-pseudos, consume unknown functional-pseudo content as balanced `Raw` | ||
- Allowed any `PseudoElement` to be a functional-pseudo (#33) | ||
- Improved walker implementations to reduce GC thrashing by reusing cursors | ||
- Changed `Atrule.block` to contain a `Block` node type only if any | ||
- Changed `Block.loc` positions to include curly brackets | ||
- Changed `Atrule.expression` to store a `null` if no expression | ||
- Changed parser to use `StyleSheet` node type only for top level node (when context is `stylesheet`, that's by default) | ||
- Changed `Parentheses`, `Brackets` and `Function` consumers to use passed sequence reader instead its own | ||
- Changed `Value` and `AtruleExpression` consumers to use common sequence reader (that reader was used by `Value` consumer only) | ||
- Changed default sequence reader to exclude storage of spaces around `Comma` | ||
- Changed processing of custom properties | ||
- Consume custom property value as balanced `Raw` | ||
- Consume `var()` fallback value as balanced `Raw` | ||
- Validate first argument of `var()` is custom property name (i.e. starts with double dash) | ||
- Custom property's value and fallback includes spaces around its value | ||
- Fixed `Nth` to have a `loc` property | ||
- Fixed `SelectorList.loc` and `Selector.loc` positions to exclude spaces | ||
- Fixed issue Browserify build fail with `default-syntax.json` is not found error (#32, @philschatz) | ||
- Disallowed `Type` selector starting with dash (parser throws an error in this case now) | ||
- Disallowed empty selectors for `Rule` (not sure if it's correct but looks reasonable) | ||
- Removed `>>` combinator support until any browser support (no signals about that yet) | ||
- Removed `PseudoElement.legacy` property | ||
- Removed special case for `:before`, `:after`, `:first-letter` and `:first-line` to represent them as `PseudoElement`, now those pseudos are represented as `PseudoClass` nodes | ||
- Removed deprecated `Syntax#match()` method | ||
- Splitted parser into modules and related changes, one step closer to an extensible parser | ||
- Various fixes and improvements, all changes have negligible impact on performace | ||
## 1.0.0-alpha13 (January 19, 2017) | ||
@@ -2,0 +36,0 @@ |
'use strict'; | ||
var List = require('../utils/list'); | ||
var Scanner = require('../scanner'); | ||
var scanner = new Scanner(); | ||
var cmpChar = Scanner.cmpChar; | ||
var endsWith = Scanner.endsWith; | ||
var isNumber = Scanner.isNumber; | ||
var isHex = Scanner.isHex; | ||
var needPositions; | ||
var filename; | ||
var TYPE = Scanner.TYPE; | ||
var WHITESPACE = TYPE.Whitespace; | ||
var IDENTIFIER = TYPE.Identifier; | ||
var COMMENT = TYPE.Comment; | ||
var HYPHENMINUS = TYPE.HyphenMinus; | ||
var DESCENDANT_COMBINATOR = {}; | ||
var SPACE_NODE = { type: 'Space' }; | ||
var NESTED = true; | ||
var ABSOLUTE = false; | ||
var RELATIVE = true; | ||
var sequence = require('./sequence'); | ||
var getAnPlusB = require('./type/An+B'); | ||
var getAtrule = require('./type/Atrule'); | ||
var getAtruleExpression = require('./type/AtruleExpression'); | ||
var getAttribute = require('./type/Attribute'); | ||
var getBlock = require('./type/Block'); | ||
var getBrackets = require('./type/Brackets'); | ||
var getClass = require('./type/Class'); | ||
var getCombinator = require('./type/Combinator'); | ||
var getComment = require('./type/Comment'); | ||
var getDeclaration = require('./type/Declaration'); | ||
var getDeclarationList = require('./type/DeclarationList'); | ||
var getDimension = require('./type/Dimention'); | ||
var getFunction = require('./type/Function'); | ||
var getHash = require('./type/Hash'); | ||
var getId = require('./type/Id'); | ||
var getIdentifier = require('./type/Identifier'); | ||
var getMediaFeature = require('./type/MediaFeature'); | ||
var getMediaQuery = require('./type/MediaQuery'); | ||
var getMediaQueryList = require('./type/MediaQueryList'); | ||
var getNth = require('./type/Nth'); | ||
var getNumber = require('./type/Number'); | ||
var getOperator = require('./type/Operator'); | ||
var getParentheses = require('./type/Parentheses'); | ||
var getPercentage = require('./type/Percentage'); | ||
var getProgid = require('./type/Progid'); | ||
var getPseudoClass = require('./type/PseudoClass'); | ||
var getPseudoElement = require('./type/PseudoElement'); | ||
var getRatio = require('./type/Ratio'); | ||
var getRaw = require('./type/Raw'); | ||
var getRule = require('./type/Rule'); | ||
var getSelector = require('./type/Selector'); | ||
var getSelectorList = require('./type/SelectorList'); | ||
var getString = require('./type/String'); | ||
var getStyleSheet = require('./type/StyleSheet'); | ||
var getType = require('./type/Type'); | ||
var getUnicodeRange = require('./type/UnicodeRange'); | ||
var getUniversal = require('./type/Universal'); | ||
var getUrl = require('./type/Url'); | ||
var getValue = require('./type/Value'); | ||
var WHITESPACE = Scanner.TYPE.Whitespace; | ||
var IDENTIFIER = Scanner.TYPE.Identifier; | ||
var NUMBER = Scanner.TYPE.Number; | ||
var STRING = Scanner.TYPE.String; | ||
var COMMENT = Scanner.TYPE.Comment; | ||
var EXCLAMATIONMARK = Scanner.TYPE.ExclamationMark; | ||
var NUMBERSIGN = Scanner.TYPE.NumberSign; | ||
var DOLLARSIGN = Scanner.TYPE.DollarSign; | ||
var PERCENTSIGN = Scanner.TYPE.PercentSign; | ||
var LEFTPARENTHESIS = Scanner.TYPE.LeftParenthesis; | ||
var RIGHTPARENTHESIS = Scanner.TYPE.RightParenthesis; | ||
var ASTERISK = Scanner.TYPE.Asterisk; | ||
var PLUSSIGN = Scanner.TYPE.PlusSign; | ||
var COMMA = Scanner.TYPE.Comma; | ||
var HYPHENMINUS = Scanner.TYPE.HyphenMinus; | ||
var FULLSTOP = Scanner.TYPE.FullStop; | ||
var SOLIDUS = Scanner.TYPE.Solidus; | ||
var COLON = Scanner.TYPE.Colon; | ||
var SEMICOLON = Scanner.TYPE.Semicolon; | ||
var EQUALSSIGN = Scanner.TYPE.EqualsSign; | ||
var GREATERTHANSIGN = Scanner.TYPE.GreaterThanSign; | ||
var QUESTIONMARK = Scanner.TYPE.QuestionMark; | ||
var COMMERCIALAT = Scanner.TYPE.CommercialAt; | ||
var LEFTSQUAREBRACKET = Scanner.TYPE.LeftSquareBracket; | ||
var RIGHTSQUAREBRACKET = Scanner.TYPE.RightSquareBracket; | ||
var CIRCUMFLEXACCENT = Scanner.TYPE.CircumflexAccent; | ||
var LEFTCURLYBRACKET = Scanner.TYPE.LeftCurlyBracket; | ||
var VERTICALLINE = Scanner.TYPE.VerticalLine; | ||
var RIGHTCURLYBRACKET = Scanner.TYPE.RightCurlyBracket; | ||
var TILDE = Scanner.TYPE.Tilde; | ||
var N = 110; // 'n'.charCodeAt(0) | ||
var SCOPE_ATRULE_EXPRESSION = { | ||
url: getUri | ||
}; | ||
var SCOPE_SELECTOR = { | ||
url: getUri, | ||
not: getSelectorListFunction, | ||
matches: getSelectorListFunction, | ||
has: getRelativeSelectorListFunction | ||
}; | ||
var SCOPE_VALUE = { | ||
url: getUri, | ||
expression: getOldIEExpression, | ||
var: getVarFunction | ||
}; | ||
var CONTEXT = { | ||
stylesheet: getStylesheet, | ||
atrule: getAtrule, | ||
atruleExpression: getAtruleExpression, | ||
rule: getRule, | ||
selectorList: getSelectorList, | ||
selector: getSelector, | ||
block: getBlock, | ||
declaration: getDeclaration, | ||
value: getValue | ||
}; | ||
function getLocation(start, end) { | ||
if (needPositions) { | ||
return scanner.getLocationRange( | ||
start, | ||
end, | ||
filename | ||
); | ||
} | ||
return null; | ||
} | ||
function getStylesheet(nested) { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var child; | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case WHITESPACE: | ||
scanner.next(); | ||
continue; | ||
case RIGHTCURLYBRACKET: | ||
if (!nested) { | ||
scanner.error('Unexpected right curly brace'); | ||
} | ||
break scan; | ||
case COMMENT: | ||
// ignore comments except exclamation comments (i.e. /*! .. */) on top level | ||
if (nested || scanner.source.charCodeAt(scanner.tokenStart + 2) !== EXCLAMATIONMARK) { | ||
scanner.next(); | ||
continue; | ||
} | ||
child = getComment(); | ||
break; | ||
case COMMERCIALAT: | ||
child = getAtrule(); | ||
break; | ||
default: | ||
child = getRule(); | ||
} | ||
children.appendData(child); | ||
} | ||
return { | ||
type: 'StyleSheet', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function isBlockAtrule() { | ||
for (var offset = 1, type; type = scanner.lookupType(offset); offset++) { | ||
if (type === RIGHTCURLYBRACKET) { | ||
return true; | ||
} | ||
if (type === LEFTCURLYBRACKET || | ||
type === COMMERCIALAT) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
function getAtruleExpression() { | ||
var start; | ||
var children = new List(); | ||
var wasSpace = false; | ||
var lastNonSpace = null; | ||
var child; | ||
readSC(); | ||
start = scanner.tokenStart; | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case SEMICOLON: | ||
case LEFTCURLYBRACKET: | ||
break scan; | ||
case WHITESPACE: | ||
wasSpace = true; | ||
scanner.next(); | ||
continue; | ||
case COMMENT: // ignore comments | ||
scanner.next(); | ||
continue; | ||
case COMMA: | ||
child = getOperator(); | ||
break; | ||
case COLON: | ||
child = getPseudo(); | ||
break; | ||
case LEFTPARENTHESIS: | ||
child = getParentheses(SCOPE_ATRULE_EXPRESSION); | ||
break; | ||
case STRING: | ||
child = getString(); | ||
break; | ||
default: | ||
child = getAny(SCOPE_ATRULE_EXPRESSION); | ||
} | ||
if (wasSpace) { | ||
wasSpace = false; | ||
children.appendData(SPACE_NODE); | ||
} | ||
lastNonSpace = scanner.tokenStart; | ||
children.appendData(child); | ||
} | ||
return { | ||
type: 'AtruleExpression', | ||
loc: getLocation(start, lastNonSpace !== null ? lastNonSpace : scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function getAtrule() { | ||
var start = scanner.tokenStart; | ||
var name; | ||
var expression; | ||
var block = null; | ||
scanner.eat(COMMERCIALAT); | ||
name = readIdent(false); | ||
expression = getAtruleExpression(); | ||
switch (scanner.tokenType) { | ||
case SEMICOLON: | ||
scanner.next(); // ; | ||
break; | ||
case LEFTCURLYBRACKET: | ||
scanner.next(); // { | ||
block = isBlockAtrule() | ||
? getBlock() | ||
: getStylesheet(NESTED); | ||
scanner.eat(RIGHTCURLYBRACKET); | ||
break; | ||
// at-rule expression can ends with semicolon, left curly bracket or eof - no other options | ||
} | ||
return { | ||
type: 'Atrule', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
expression: expression, | ||
block: block | ||
}; | ||
} | ||
function getRule() { | ||
var start = scanner.tokenStart; | ||
var selector = getSelectorList(); | ||
var block; | ||
scanner.eat(LEFTCURLYBRACKET); | ||
block = getBlock(); | ||
scanner.eat(RIGHTCURLYBRACKET); | ||
return { | ||
type: 'Rule', | ||
loc: getLocation(start, scanner.tokenStart), | ||
selector: selector, | ||
block: block | ||
}; | ||
} | ||
function getSelectorList(nested, relative) { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var selector; | ||
var lastComma = -2; | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case LEFTCURLYBRACKET: | ||
if (nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case RIGHTPARENTHESIS: | ||
if (!nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case COMMA: | ||
if (lastComma !== -1) { | ||
scanner.error('Unexpected comma'); | ||
} | ||
lastComma = scanner.tokenStart; | ||
scanner.next(); | ||
break; | ||
default: | ||
lastComma = -1; | ||
selector = getSelector(nested, relative); | ||
children.appendData(selector); | ||
if (selector.children.isEmpty()) { | ||
scanner.error('Simple selector expected'); | ||
} | ||
} | ||
} | ||
if (lastComma !== -1 && lastComma !== -2) { // TODO: fail on empty selector rules? | ||
scanner.error('Unexpected trailing comma', lastComma); | ||
} | ||
return { | ||
type: 'SelectorList', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function getSelector(nested, relative) { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var combinator = null; | ||
var combinatorOffset = -1; | ||
var child; | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case COMMA: | ||
break scan; | ||
case LEFTCURLYBRACKET: | ||
if (nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case RIGHTPARENTHESIS: | ||
if (!nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case COMMENT: | ||
scanner.next(); | ||
continue; | ||
case WHITESPACE: | ||
if (combinator === null && children.head !== null) { | ||
combinatorOffset = scanner.tokenStart; | ||
combinator = DESCENDANT_COMBINATOR; | ||
} else { | ||
scanner.next(); | ||
} | ||
continue; | ||
case PLUSSIGN: | ||
case GREATERTHANSIGN: | ||
case TILDE: | ||
case SOLIDUS: | ||
if ((children.head === null && !relative) || // combinator in the beginning | ||
(combinator !== null && combinator !== DESCENDANT_COMBINATOR)) { | ||
scanner.error('Unexpected combinator'); | ||
} | ||
combinatorOffset = scanner.tokenStart; | ||
combinator = getCombinator(); | ||
continue; | ||
case FULLSTOP: | ||
child = getClass(); | ||
break; | ||
case LEFTSQUAREBRACKET: | ||
child = getAttribute(); | ||
break; | ||
case NUMBERSIGN: | ||
child = getId(); | ||
break; | ||
case COLON: | ||
child = getPseudo(); | ||
break; | ||
case HYPHENMINUS: | ||
case IDENTIFIER: | ||
case ASTERISK: | ||
case VERTICALLINE: | ||
child = getTypeOrUniversal(); | ||
break; | ||
case NUMBER: | ||
child = getPercentage(); | ||
break; | ||
default: | ||
scanner.error(); | ||
} | ||
if (combinator !== null) { | ||
// create descendant combinator on demand to avoid garbage | ||
if (combinator === DESCENDANT_COMBINATOR) { | ||
combinator = { | ||
type: 'Combinator', | ||
loc: getLocation(combinatorOffset, combinatorOffset + 1), | ||
name: ' ' | ||
}; | ||
} | ||
children.appendData(combinator); | ||
combinator = null; | ||
} | ||
children.appendData(child); | ||
} | ||
if (combinator !== null && combinator !== DESCENDANT_COMBINATOR) { | ||
scanner.error('Unexpected combinator', combinatorOffset); | ||
} | ||
return { | ||
type: 'Selector', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function getCompoundSelector(nested) { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var child; | ||
readSC(); | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case COMMA: | ||
break scan; | ||
case LEFTCURLYBRACKET: | ||
if (nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case RIGHTPARENTHESIS: | ||
if (!nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case COMMENT: | ||
scanner.next(); | ||
continue; | ||
case WHITESPACE: | ||
readSC(); | ||
break scan; | ||
case FULLSTOP: | ||
child = getClass(); | ||
break; | ||
case LEFTSQUAREBRACKET: | ||
child = getAttribute(); | ||
break; | ||
case NUMBERSIGN: | ||
child = getId(); | ||
break; | ||
case COLON: | ||
child = getPseudo(); | ||
break; | ||
case HYPHENMINUS: | ||
case IDENTIFIER: | ||
case ASTERISK: | ||
case VERTICALLINE: | ||
child = getTypeOrUniversal(); | ||
break; | ||
default: | ||
scanner.error(); | ||
} | ||
children.appendData(child); | ||
} | ||
if (children.isEmpty()) { | ||
scanner.error('Simple selector expected'); | ||
} | ||
return { | ||
type: 'Selector', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function getBlock() { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case RIGHTCURLYBRACKET: | ||
break scan; | ||
case WHITESPACE: | ||
case COMMENT: | ||
case SEMICOLON: | ||
scanner.next(); | ||
break; | ||
case COMMERCIALAT: | ||
children.appendData(getAtrule()); | ||
break; | ||
default: | ||
children.appendData(getDeclaration()); | ||
} | ||
} | ||
return { | ||
type: 'Block', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function getDeclaration(nested) { | ||
var start = scanner.tokenStart; | ||
var property = readProperty(); | ||
var important = false; | ||
var value; | ||
readSC(); | ||
scanner.eat(COLON); | ||
value = getValue(nested, property); | ||
if (scanner.tokenType === EXCLAMATIONMARK) { | ||
important = getImportant(); | ||
} | ||
// TODO: include or not to include semicolon to range? | ||
// if (scanner.tokenType === SEMICOLON) { | ||
// scanner.next(); | ||
// } | ||
return { | ||
type: 'Declaration', | ||
loc: getLocation(start, scanner.tokenStart), | ||
important: important, | ||
property: property, | ||
value: value | ||
}; | ||
} | ||
function readProperty() { | ||
var start = scanner.tokenStart; | ||
var type; | ||
for (; type = scanner.tokenType; scanner.next()) { | ||
if (type !== SOLIDUS && | ||
type !== ASTERISK && | ||
type !== DOLLARSIGN) { | ||
break; | ||
} | ||
} | ||
scanIdent(true); | ||
return scanner.substrToCursor(start); | ||
} | ||
function getValue(nested, property) { | ||
// special parser for filter property since it can contains non-standart syntax for old IE | ||
if (property !== null && endsWith(property, 'filter') && checkProgid()) { | ||
return getFilterValue(); | ||
} | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var wasSpace = false; | ||
var child; | ||
readSC(); | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case RIGHTCURLYBRACKET: | ||
case SEMICOLON: | ||
case EXCLAMATIONMARK: | ||
break scan; | ||
case RIGHTPARENTHESIS: | ||
if (!nested) { | ||
scanner.error(); | ||
} | ||
break scan; | ||
case WHITESPACE: | ||
wasSpace = true; | ||
scanner.next(); | ||
continue; | ||
case COMMENT: // ignore comments | ||
scanner.next(); | ||
continue; | ||
case NUMBERSIGN: | ||
child = getHash(); | ||
break; | ||
case SOLIDUS: | ||
case COMMA: | ||
child = getOperator(); | ||
break; | ||
case LEFTPARENTHESIS: | ||
child = getParentheses(SCOPE_VALUE); | ||
break; | ||
case LEFTSQUAREBRACKET: | ||
child = getBrackets(); | ||
break; | ||
case STRING: | ||
child = getString(); | ||
break; | ||
default: | ||
// check for unicode range: U+0F00, U+0F00-0FFF, u+0F00?? | ||
if (scanner.tokenType === IDENTIFIER && | ||
scanner.lookupValue(0, 'u')) { | ||
if ( | ||
scanner.lookupType(1) === PLUSSIGN || ( | ||
scanner.lookupType(1) === NUMBER && | ||
cmpChar(scanner.source, scanner.tokenEnd, PLUSSIGN) | ||
)) { | ||
child = getUnicodeRange(); | ||
break; | ||
} | ||
} | ||
child = getAny(SCOPE_VALUE); | ||
} | ||
if (wasSpace) { | ||
wasSpace = false; | ||
children.appendData(SPACE_NODE); | ||
} | ||
children.appendData(child); | ||
} | ||
return { | ||
type: 'Value', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
function getFilterValue() { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var progid; | ||
while (progid = checkProgid()) { | ||
readSC(); | ||
children.appendData(getProgid(progid)); | ||
} | ||
readSC(); | ||
return { | ||
type: 'Value', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
// any = percentage | dimension | number | operator | ident | function | ||
function getAny(scope) { | ||
switch (scanner.tokenType) { | ||
case IDENTIFIER: | ||
break; | ||
case HYPHENMINUS: | ||
var nextType = scanner.lookupType(1); | ||
if (nextType === IDENTIFIER || nextType === HYPHENMINUS) { | ||
break; | ||
} | ||
return getOperator(); | ||
case PLUSSIGN: | ||
return getOperator(); | ||
case NUMBER: | ||
switch (scanner.lookupType(1)) { | ||
case PERCENTSIGN: | ||
return getPercentage(); | ||
case IDENTIFIER: | ||
return getDimension(); | ||
default: | ||
return { | ||
type: 'Number', | ||
loc: getLocation(scanner.tokenStart, scanner.tokenEnd), | ||
value: readNumber() | ||
}; | ||
} | ||
default: | ||
scanner.error(); | ||
} | ||
var start = scanner.tokenStart; | ||
scanIdent(false); | ||
if (scanner.tokenType === LEFTPARENTHESIS) { | ||
return getFunction(scope, start, scanner.substrToCursor(start)); | ||
} | ||
return { | ||
type: 'Identifier', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: scanner.substrToCursor(start) | ||
}; | ||
} | ||
function readAttrselector() { | ||
var start = scanner.tokenStart; | ||
var tokenType = scanner.tokenType; | ||
if (tokenType !== EQUALSSIGN && // = | ||
tokenType !== TILDE && // ~= | ||
tokenType !== CIRCUMFLEXACCENT && // ^= | ||
tokenType !== DOLLARSIGN && // $= | ||
tokenType !== ASTERISK && // *= | ||
tokenType !== VERTICALLINE // |= | ||
) { | ||
scanner.error('Attribute selector (=, ~=, ^=, $=, *=, |=) is expected'); | ||
} | ||
if (tokenType === EQUALSSIGN) { | ||
scanner.next(); | ||
} else { | ||
scanner.next(); | ||
scanner.eat(EQUALSSIGN); | ||
} | ||
return scanner.substrToCursor(start); | ||
} | ||
// '[' S* attrib_name ']' | ||
// '[' S* attrib_name S* attrib_match S* [ IDENT | STRING ] S* attrib_flags? S* ']' | ||
function getAttribute() { | ||
var start = scanner.tokenStart; | ||
var name; | ||
var operator = null; | ||
var value = null; | ||
var flags = null; | ||
scanner.eat(LEFTSQUAREBRACKET); | ||
readSC(); | ||
name = getAttributeName(); | ||
readSC(); | ||
if (scanner.tokenType !== RIGHTSQUAREBRACKET) { | ||
// avoid case `[name i]` | ||
if (scanner.tokenType !== IDENTIFIER) { | ||
operator = readAttrselector(); | ||
readSC(); | ||
value = scanner.tokenType === STRING | ||
? getString() | ||
: getIdentifier(false); | ||
readSC(); | ||
} | ||
// attribute flags | ||
if (scanner.tokenType === IDENTIFIER) { | ||
flags = scanner.getTokenValue(); | ||
scanner.next(); | ||
readSC(); | ||
} | ||
} | ||
scanner.eat(RIGHTSQUAREBRACKET); | ||
return { | ||
type: 'Attribute', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
operator: operator, | ||
value: value, | ||
flags: flags | ||
}; | ||
} | ||
function getParentheses(scope) { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var wasSpace = false; | ||
var child; | ||
// left parenthesis | ||
scanner.eat(LEFTPARENTHESIS); | ||
readSC(); | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case RIGHTPARENTHESIS: | ||
break scan; | ||
case WHITESPACE: | ||
wasSpace = true; | ||
scanner.next(); | ||
continue; | ||
case COMMENT: // ignore comments | ||
scanner.next(); | ||
continue; | ||
case LEFTPARENTHESIS: | ||
child = getParentheses(scope); | ||
break; | ||
case SOLIDUS: | ||
case ASTERISK: | ||
case COMMA: | ||
case COLON: | ||
child = getOperator(); | ||
break; | ||
case NUMBERSIGN: | ||
child = getHash(); | ||
break; | ||
case STRING: | ||
child = getString(); | ||
break; | ||
default: | ||
child = getAny(scope); | ||
} | ||
if (wasSpace) { | ||
wasSpace = false; | ||
children.appendData(SPACE_NODE); | ||
} | ||
children.appendData(child); | ||
} | ||
// right parenthesis | ||
scanner.eat(RIGHTPARENTHESIS); | ||
return { | ||
type: 'Parentheses', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
// currently only Grid Layout uses square brackets | ||
// https://drafts.csswg.org/css-grid/#track-sizing | ||
// [ ident* ] | ||
function getBrackets() { | ||
var start = scanner.tokenStart; | ||
var children = new List(); | ||
var wasSpace = false; | ||
var child; | ||
// left bracket | ||
scanner.eat(LEFTSQUAREBRACKET); | ||
readSC(); | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case RIGHTSQUAREBRACKET: | ||
break scan; | ||
case WHITESPACE: | ||
wasSpace = true; | ||
scanner.next(); | ||
continue; | ||
case COMMENT: // ignore comments | ||
scanner.next(); | ||
continue; | ||
default: | ||
child = getIdentifier(false); | ||
} | ||
if (wasSpace) { | ||
wasSpace = false; | ||
children.appendData(SPACE_NODE); | ||
} | ||
children.appendData(child); | ||
} | ||
// right bracket | ||
scanner.eat(RIGHTSQUAREBRACKET); | ||
return { | ||
type: 'Brackets', | ||
loc: getLocation(start, scanner.tokenStart), | ||
children: children | ||
}; | ||
} | ||
// '.' ident | ||
function getClass() { | ||
var start = scanner.tokenStart; | ||
var name; | ||
scanner.eat(FULLSTOP); | ||
name = readIdent(false); | ||
return { | ||
type: 'Class', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name | ||
}; | ||
} | ||
// '#' ident | ||
function getId() { | ||
var start = scanner.tokenStart; | ||
var name; | ||
scanner.eat(NUMBERSIGN); | ||
name = readIdent(false); | ||
return { | ||
type: 'Id', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name | ||
}; | ||
} | ||
// + | > | ~ | /deep/ | >> | ||
function getCombinator() { | ||
var start = scanner.tokenStart; | ||
var combinator; | ||
switch (scanner.tokenType) { | ||
case GREATERTHANSIGN: | ||
scanner.next(); | ||
if (scanner.tokenType === GREATERTHANSIGN) { | ||
combinator = '>>'; | ||
scanner.next(); | ||
} else { | ||
combinator = '>'; | ||
} | ||
break; | ||
case PLUSSIGN: | ||
case TILDE: | ||
combinator = scanner.getTokenValue(); | ||
scanner.next(); | ||
break; | ||
case SOLIDUS: | ||
combinator = '/deep/'; | ||
scanner.next(); | ||
scanner.expectIdentifier('deep'); | ||
scanner.eat(SOLIDUS); | ||
break; | ||
} | ||
return { | ||
type: 'Combinator', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: combinator | ||
}; | ||
} | ||
// '/*' .* '*/' | ||
function getComment() { | ||
var start = scanner.tokenStart; | ||
var end = scanner.tokenEnd; | ||
if ((end - start + 2) >= 2 && | ||
scanner.source.charCodeAt(end - 2) === ASTERISK && | ||
scanner.source.charCodeAt(end - 1) === SOLIDUS) { | ||
end -= 2; | ||
} | ||
scanner.next(); | ||
return { | ||
type: 'Comment', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: scanner.source.substring(start + 2, end) | ||
}; | ||
} | ||
// special reader for units to avoid adjoined IE hacks (i.e. '1px\9') | ||
function readUnit() { | ||
var unit = scanner.getTokenValue(); | ||
var backSlashPos = unit.indexOf('\\'); | ||
if (backSlashPos !== -1) { | ||
// patch token offset | ||
scanner.tokenStart += backSlashPos; | ||
// scanner.token.start = scanner.tokenStart; | ||
// return part before backslash | ||
return unit.substring(0, backSlashPos); | ||
} | ||
// no backslash in unit name | ||
scanner.next(); | ||
return unit; | ||
} | ||
// number ident | ||
function getDimension() { | ||
var start = scanner.tokenStart; | ||
var value = readNumber(); | ||
var unit = readUnit(); | ||
return { | ||
type: 'Dimension', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: value, | ||
unit: unit | ||
}; | ||
} | ||
function getPercentage() { | ||
var start = scanner.tokenStart; | ||
var number = readNumber(); | ||
scanner.eat(PERCENTSIGN); | ||
return { | ||
type: 'Percentage', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: number | ||
}; | ||
} | ||
// ident '(' functionBody ')' | ||
function getFunction(scope, start, name) { | ||
// parse special functions | ||
var nameLowerCase = name.toLowerCase(); | ||
switch (scope) { | ||
case SCOPE_SELECTOR: | ||
if (SCOPE_SELECTOR.hasOwnProperty(nameLowerCase)) { | ||
return SCOPE_SELECTOR[nameLowerCase](scope, start, name); | ||
} | ||
break; | ||
case SCOPE_VALUE: | ||
if (SCOPE_VALUE.hasOwnProperty(nameLowerCase)) { | ||
return SCOPE_VALUE[nameLowerCase](scope, start, name); | ||
} | ||
break; | ||
case SCOPE_ATRULE_EXPRESSION: | ||
if (SCOPE_ATRULE_EXPRESSION.hasOwnProperty(nameLowerCase)) { | ||
return SCOPE_ATRULE_EXPRESSION[nameLowerCase](scope, start, name); | ||
} | ||
break; | ||
} | ||
return getFunctionInternal(getFunctionArguments, scope, start, name); | ||
} | ||
function getFunctionInternal(readSequence, scope, start, name) { | ||
var children; | ||
scanner.eat(LEFTPARENTHESIS); | ||
children = readSequence(scope); | ||
scanner.eat(RIGHTPARENTHESIS); | ||
return { | ||
type: scope === SCOPE_SELECTOR ? 'PseudoClass' : 'Function', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
children: children | ||
}; | ||
} | ||
function getFunctionArguments(scope) { | ||
var children = new List(); | ||
var wasSpace = false; | ||
var prevNonSpaceOperator = false; | ||
var nonSpaceOperator = false; | ||
var child; | ||
readSC(); | ||
scan: | ||
while (!scanner.eof) { | ||
switch (scanner.tokenType) { | ||
case RIGHTPARENTHESIS: | ||
break scan; | ||
case WHITESPACE: | ||
wasSpace = true; | ||
scanner.next(); | ||
continue; | ||
case COMMENT: // ignore comments | ||
scanner.next(); | ||
continue; | ||
case NUMBERSIGN: // TODO: not sure it should be here | ||
child = getHash(); | ||
break; | ||
case LEFTPARENTHESIS: | ||
child = getParentheses(scope); | ||
break; | ||
case COMMA: | ||
case SOLIDUS: | ||
case ASTERISK: | ||
wasSpace = false; | ||
nonSpaceOperator = true; | ||
child = getOperator(); | ||
break; | ||
case STRING: | ||
child = getString(); | ||
break; | ||
default: | ||
child = getAny(scope); | ||
} | ||
if (wasSpace) { | ||
wasSpace = false; | ||
// ignore spaces around operator | ||
if (!nonSpaceOperator && !prevNonSpaceOperator) { | ||
children.appendData(SPACE_NODE); | ||
} | ||
} | ||
children.appendData(child); | ||
prevNonSpaceOperator = nonSpaceOperator; | ||
nonSpaceOperator = false; | ||
} | ||
return children; | ||
} | ||
function getSelectorListFunction(scope, start, name) { | ||
return getFunctionInternal(function() { | ||
return new List().appendData(getSelectorList(NESTED, ABSOLUTE)); | ||
}, scope, start, name); | ||
} | ||
function getRelativeSelectorListFunction(scope, start, name) { | ||
return getFunctionInternal(function() { | ||
return new List().appendData(getSelectorList(NESTED, RELATIVE)); | ||
}, scope, start, name); | ||
} | ||
function getVarFunction(scope, start, name) { | ||
return getFunctionInternal(getVarFunctionArguments, scope, start, name); | ||
} | ||
// var '(' ident (',' <declaration-value>)? ')' | ||
function getVarFunctionArguments() { // TODO: special type Variable? | ||
var children = new List(); | ||
readSC(); | ||
children.appendData(getIdentifier(true)); | ||
readSC(); | ||
if (scanner.tokenType === COMMA) { | ||
children.appendData(getOperator()); | ||
readSC(); | ||
children.appendData(getValue(true, null)); | ||
readSC(); | ||
} | ||
return children; | ||
} | ||
// url '(' ws* (string | raw) ws* ')' | ||
function getUri(scope, start) { | ||
var value; | ||
scanner.eat(LEFTPARENTHESIS); // ( | ||
readSC(); | ||
if (scanner.tokenType === STRING) { | ||
value = getString(); | ||
} else { | ||
var rawStart = scanner.tokenStart; | ||
// TODO: fix me, looks like incorrect raw scan | ||
for (; !scanner.eof; scanner.next()) { | ||
var type = scanner.tokenType; | ||
if (type === WHITESPACE || | ||
type === LEFTPARENTHESIS || | ||
type === RIGHTPARENTHESIS) { | ||
break; | ||
} | ||
} | ||
value = { | ||
type: 'Raw', | ||
loc: getLocation(rawStart, scanner.tokenStart), | ||
value: scanner.substrToCursor(rawStart) | ||
}; | ||
} | ||
readSC(); | ||
scanner.eat(RIGHTPARENTHESIS); // ) | ||
return { | ||
type: 'Url', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: value | ||
}; | ||
} | ||
// expression '(' raw ')' | ||
function getOldIEExpression(scope, start, name) { | ||
var rawStart = scanner.tokenStart + 1; // skip open parenthesis | ||
var rawNode; | ||
scanner.eat(LEFTPARENTHESIS); | ||
for (var balance = 0; !scanner.eof; scanner.next()) { | ||
if (scanner.tokenType === RIGHTPARENTHESIS) { | ||
if (balance === 0) { | ||
break; | ||
} | ||
balance--; | ||
} else if (scanner.tokenType === LEFTPARENTHESIS) { | ||
balance++; | ||
} | ||
} | ||
rawNode = { | ||
type: 'Raw', | ||
loc: getLocation(rawStart, scanner.tokenStart), | ||
value: scanner.substrToCursor(rawStart) | ||
}; | ||
scanner.eat(RIGHTPARENTHESIS); | ||
return { | ||
type: 'Function', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
children: new List().appendData(rawNode) | ||
}; | ||
} | ||
function scanUnicodeNumber() { | ||
for (var pos = scanner.tokenStart + 1; pos < scanner.tokenEnd; pos++) { | ||
var code = scanner.source.charCodeAt(pos); | ||
// break on fullstop or hyperminus/plussign after exponent | ||
if (code === FULLSTOP || code === PLUSSIGN) { | ||
// break token, exclude symbol | ||
scanner.tokenStart = pos; | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
// https://drafts.csswg.org/css-syntax-3/#urange | ||
function scanUnicodeRange() { | ||
var hexStart = scanner.tokenStart + 1; // skip + | ||
var hexLength = 0; | ||
scan: { | ||
if (scanner.tokenType === NUMBER) { | ||
if (scanner.source.charCodeAt(scanner.tokenStart) !== FULLSTOP && scanUnicodeNumber()) { | ||
scanner.next(); | ||
} else if (scanner.source.charCodeAt(scanner.tokenStart) !== HYPHENMINUS) { | ||
break scan; | ||
} | ||
} else { | ||
scanner.next(); // PLUSSIGN | ||
} | ||
if (scanner.tokenType === HYPHENMINUS) { | ||
scanner.next(); | ||
} | ||
if (scanner.tokenType === NUMBER) { | ||
scanner.next(); | ||
} | ||
if (scanner.tokenType === IDENTIFIER) { | ||
scanner.next(); | ||
} | ||
if (scanner.tokenStart === hexStart) { | ||
scanner.error('Unexpected input', hexStart); | ||
} | ||
} | ||
// validate for U+x{1,6} or U+x{1,6}-x{1,6} | ||
// where x is [0-9a-fA-F] | ||
// TODO: check hex sequence length | ||
for (var i = hexStart, wasHyphenMinus = false; i < scanner.tokenStart; i++) { | ||
var code = scanner.source.charCodeAt(i); | ||
if (isHex(code) === false && (code !== HYPHENMINUS || wasHyphenMinus)) { | ||
scanner.error('Unexpected input', i); | ||
} | ||
if (code === HYPHENMINUS) { | ||
// hex sequence shouldn't be an empty | ||
if (hexLength === 0) { | ||
scanner.error('Unexpected input', i); | ||
} | ||
wasHyphenMinus = true; | ||
hexLength = 0; | ||
} else { | ||
hexLength++; | ||
// to long hex sequence | ||
if (hexLength > 6) { | ||
scanner.error('Unexpected input', i); | ||
} | ||
} | ||
} | ||
// check we have a non-zero sequence | ||
if (hexLength === 0) { | ||
scanner.error('Unexpected input', i - 1); | ||
} | ||
// U+abc??? | ||
if (!wasHyphenMinus) { | ||
// consume as many U+003F QUESTION MARK (?) code points as possible | ||
for (; hexLength < 6 && !scanner.eof; scanner.next()) { | ||
if (scanner.tokenType !== QUESTIONMARK) { | ||
break; | ||
} | ||
hexLength++; | ||
} | ||
} | ||
} | ||
function getUnicodeRange() { | ||
var start = scanner.tokenStart; | ||
scanner.next(); // U or u | ||
scanUnicodeRange(); | ||
return { | ||
type: 'UnicodeRange', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: scanner.substrToCursor(start) | ||
}; | ||
} | ||
function scanIdent(varAllowed) { | ||
// optional first - | ||
if (scanner.tokenType === HYPHENMINUS) { | ||
scanner.next(); | ||
if (this.scanner.tokenType === HYPHENMINUS) { | ||
this.scanner.next(); | ||
// variable -- | ||
if (varAllowed && scanner.tokenType === HYPHENMINUS) { | ||
scanner.next(); | ||
if (varAllowed && this.scanner.tokenType === HYPHENMINUS) { | ||
this.scanner.next(); | ||
} | ||
} | ||
scanner.eat(IDENTIFIER); | ||
this.scanner.eat(IDENTIFIER); | ||
} | ||
function readIdent(varAllowed) { | ||
var start = scanner.tokenStart; | ||
var start = this.scanner.tokenStart; | ||
scanIdent(varAllowed); | ||
this.scanIdent(varAllowed); | ||
return scanner.substrToCursor(start); | ||
return this.scanner.substrToCursor(start); | ||
} | ||
function getAttributeName() { | ||
if (scanner.eof) { | ||
scanner.error('Unexpected end of input'); | ||
function readSC() { | ||
while (this.scanner.tokenType === WHITESPACE || this.scanner.tokenType === COMMENT) { | ||
this.scanner.next(); | ||
} | ||
var start = scanner.tokenStart; | ||
var expectIdentifier = false; | ||
var checkColon = true; | ||
if (scanner.tokenType === ASTERISK) { | ||
expectIdentifier = true; | ||
checkColon = false; | ||
scanner.next(); | ||
} else if (scanner.tokenType !== VERTICALLINE) { | ||
scanIdent(false); | ||
} | ||
if (scanner.tokenType === VERTICALLINE) { | ||
if (scanner.lookupType(1) !== EQUALSSIGN) { | ||
scanner.next(); | ||
if (scanner.tokenType === HYPHENMINUS || scanner.tokenType === IDENTIFIER) { | ||
scanIdent(false); | ||
} else { | ||
scanner.error('Identifier is expected'); | ||
} | ||
} else if (expectIdentifier) { | ||
scanner.error('Identifier is expected', scanner.tokenEnd); | ||
} | ||
} else if (expectIdentifier) { | ||
scanner.error('Vertical line is expected'); | ||
} | ||
if (checkColon && scanner.tokenType === COLON) { | ||
scanner.next(); | ||
scanIdent(false); | ||
} | ||
return { | ||
type: 'Identifier', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: scanner.substrToCursor(start) | ||
}; | ||
} | ||
function getTypeOrUniversal() { | ||
var start = scanner.tokenStart; | ||
var universal = false; | ||
var Parser = function() { | ||
this.scanner = new Scanner(); | ||
this.needPositions = false; | ||
this.filename = '<unknown>'; | ||
}; | ||
if (scanner.tokenType === ASTERISK) { | ||
universal = true; | ||
scanner.next(); | ||
} else if (scanner.tokenType !== VERTICALLINE) { | ||
scanIdent(false); | ||
} | ||
Parser.prototype = { | ||
SPACE_NODE: Object.freeze({ type: 'Space' }), | ||
if (scanner.tokenType === VERTICALLINE) { | ||
universal = false; | ||
scanner.next(); | ||
scopeAtruleExpression: {}, | ||
scopeValue: { | ||
expression: require('./function/expression'), | ||
var: require('./function/var') | ||
}, | ||
atrule: { | ||
'import': require('./atrule/import'), | ||
'media': require('./atrule/media'), | ||
'page': require('./atrule/page'), | ||
'supports': require('./atrule/supports') | ||
}, | ||
pseudo: { | ||
'lang': sequence.singleIdentifier, | ||
'dir': sequence.singleIdentifier, | ||
'not': sequence.selectorList, | ||
'matches': sequence.selectorList, | ||
'has': sequence.relativeSelectorList, | ||
'nth-child': sequence.nthWithOfClause, | ||
'nth-last-child': sequence.nthWithOfClause, | ||
'nth-of-type': sequence.nth, | ||
'nth-last-of-type': sequence.nth, | ||
'slotted': sequence.compoundSelectorList | ||
}, | ||
context: { | ||
stylesheet: getStyleSheet, | ||
atrule: getAtrule, | ||
atruleExpression: getAtruleExpression, | ||
rule: getRule, | ||
selectorList: getSelectorList, | ||
selector: getSelector, | ||
block: getBlock, | ||
declarationList: getDeclarationList, | ||
declaration: getDeclaration, | ||
value: getValue | ||
}, | ||
if (scanner.tokenType === HYPHENMINUS || scanner.tokenType === IDENTIFIER) { | ||
scanIdent(false); | ||
} else if (scanner.tokenType === ASTERISK) { | ||
universal = true; | ||
scanner.next(); | ||
} else { | ||
scanner.error('Identifier or asterisk is expected'); | ||
} | ||
} | ||
// consumers | ||
AnPlusB: getAnPlusB, | ||
Atrule: getAtrule, | ||
AtruleExpression: getAtruleExpression, | ||
Attribute: getAttribute, | ||
Block: getBlock, | ||
Brackets: getBrackets, | ||
Class: getClass, | ||
Combinator: getCombinator, | ||
Comment: getComment, | ||
Declaration: getDeclaration, | ||
DeclarationList: getDeclarationList, | ||
Dimension: getDimension, | ||
Function: getFunction, | ||
Hash: getHash, | ||
Id: getId, | ||
Identifier: getIdentifier, | ||
MediaFeature: getMediaFeature, | ||
MediaQuery: getMediaQuery, | ||
MediaQueryList: getMediaQueryList, | ||
Nth: getNth, | ||
Number: getNumber, | ||
Operator: getOperator, | ||
Parentheses: getParentheses, | ||
Percentage: getPercentage, | ||
Progid: getProgid, | ||
PseudoClass: getPseudoClass, | ||
PseudoElement: getPseudoElement, | ||
Ratio: getRatio, | ||
Raw: getRaw, | ||
Rule: getRule, | ||
Selector: getSelector, | ||
SelectorList: getSelectorList, | ||
String: getString, | ||
Stylesheet: getStyleSheet, | ||
Type: getType, | ||
UnicodeRange: getUnicodeRange, | ||
Universal: getUniversal, | ||
Url: getUrl, | ||
Value: getValue, | ||
return { | ||
type: universal ? 'Universal' : 'Type', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: scanner.substrToCursor(start) | ||
}; | ||
} | ||
scanIdent: scanIdent, | ||
readIdent: readIdent, | ||
readSC: readSC, | ||
readSequence: sequence.default, | ||
function getIdentifier(varAllowed) { | ||
var start = scanner.tokenStart; | ||
var name = readIdent(varAllowed); | ||
return { | ||
type: 'Identifier', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name | ||
}; | ||
} | ||
// ! ws* important | ||
function getImportant() { | ||
scanner.eat(EXCLAMATIONMARK); | ||
readSC(); | ||
scanner.expectIdentifier('important'); | ||
// should return identifier in future for original source restoring as is | ||
// returns true for now since it's fit to optimizer purposes | ||
return true; | ||
} | ||
function checkTokenIsInteger() { | ||
var pos = scanner.tokenStart; | ||
if (scanner.source.charCodeAt(pos) === PLUSSIGN || | ||
scanner.source.charCodeAt(pos) === HYPHENMINUS) { | ||
pos++; | ||
} | ||
for (; pos < scanner.tokenEnd; pos++) { | ||
if (!isNumber(scanner.source.charCodeAt(pos))) { | ||
scanner.error('Unexpected input', pos); | ||
getLocation: function getLocation(start, end) { | ||
if (this.needPositions) { | ||
return this.scanner.getLocationRange( | ||
start, | ||
end, | ||
this.filename | ||
); | ||
} | ||
} | ||
} | ||
// https://drafts.csswg.org/css-syntax-3/#the-anb-type | ||
function getNthSelector(allowOfClause) { | ||
var start = scanner.tokenStart; | ||
var selector = null; | ||
var query; | ||
var name; | ||
return null; | ||
}, | ||
parse: function parse(source, options) { | ||
options = options || {}; | ||
scanner.eat(COLON); | ||
name = readIdent(false); | ||
scanner.eat(LEFTPARENTHESIS); | ||
readSC(); | ||
var context = options.context || 'stylesheet'; | ||
var ast; | ||
var nthStart = scanner.tokenStart; | ||
this.scanner.setSource(source, options.line, options.column); | ||
this.filename = options.filename || '<unknown>'; | ||
this.needPositions = Boolean(options.positions); | ||
if (scanner.lookupValue(0, 'odd') || scanner.lookupValue(0, 'even')) { | ||
scanner.next(); | ||
query = { | ||
type: 'Identifier', | ||
loc: getLocation(nthStart, scanner.tokenStart), | ||
name: scanner.substrToCursor(nthStart) | ||
}; | ||
} else { | ||
// scan An+B microsyntax https://www.w3.org/TR/css-syntax-3/#anb | ||
var prefix = ''; | ||
var a = null; | ||
var b = null; | ||
switch (context) { | ||
case 'value': | ||
ast = this.Value(options.property ? String(options.property) : null); | ||
break; | ||
if (scanner.tokenType === HYPHENMINUS || | ||
scanner.tokenType === PLUSSIGN || | ||
scanner.tokenType === NUMBER) { | ||
checkTokenIsInteger(); | ||
prefix = scanner.getTokenValue(); | ||
scanner.next(); | ||
} | ||
case 'atruleExpression': | ||
ast = this.Value(options.atrule ? String(options.atrule) : null); | ||
break; | ||
if (scanner.tokenType === IDENTIFIER) { | ||
if (!cmpChar(scanner.source, scanner.tokenStart, N)) { | ||
scanner.error(); | ||
} | ||
a = prefix === '' || prefix === '+' ? '1' : | ||
prefix === '-' ? '-1' : | ||
prefix; | ||
var len = scanner.tokenEnd - scanner.tokenStart; | ||
if (len > 1) { | ||
var bStart = scanner.tokenStart; | ||
// ..n-.. | ||
if (scanner.source.charCodeAt(bStart + 1) !== HYPHENMINUS) { | ||
scanner.error('Unexpected input', bStart + 1); | ||
default: | ||
if (!this.context.hasOwnProperty(context)) { | ||
throw new Error('Unknown context `' + context + '`'); | ||
} | ||
scanner.tokenStart = bStart + 1; | ||
// ..n-{number}.. | ||
if (len > 2) { | ||
for (var i = bStart + 2; i < scanner.tokenEnd; i++) { | ||
if (!isNumber(scanner.source.charCodeAt(i))) { | ||
scanner.error('Unexpected input', i); | ||
} | ||
} | ||
scanner.next(); | ||
b = '-' + scanner.substrToCursor(bStart + 2); | ||
} else { | ||
scanner.next(); | ||
readSC(); | ||
if (scanner.tokenType !== NUMBER || | ||
cmpChar(scanner.source, scanner.tokenStart, PLUSSIGN) || | ||
cmpChar(scanner.source, scanner.tokenStart, HYPHENMINUS)) { | ||
scanner.error(); | ||
} | ||
b = '-' + scanner.getTokenValue(); | ||
scanner.next(); | ||
} | ||
} else { | ||
prefix = ''; | ||
scanner.next(); | ||
readSC(); | ||
if (scanner.tokenType === HYPHENMINUS || | ||
scanner.tokenType === PLUSSIGN) { | ||
prefix = scanner.getTokenValue(); | ||
scanner.next(); | ||
readSC(); | ||
} | ||
if (scanner.tokenType === NUMBER) { | ||
checkTokenIsInteger(); | ||
if (cmpChar(scanner.source, scanner.tokenStart, PLUSSIGN) || | ||
cmpChar(scanner.source, scanner.tokenStart, HYPHENMINUS)) { | ||
// prefix or sign should be specified but not both | ||
if (prefix !== '') { | ||
scanner.error(); | ||
} | ||
prefix = scanner.source.charAt(scanner.tokenStart); | ||
scanner.tokenStart++; | ||
} | ||
if (prefix === '') { | ||
// should be an operator before number | ||
scanner.error(); | ||
} else if (prefix === '+') { | ||
// plus is using by default | ||
prefix = ''; | ||
} | ||
b = prefix + scanner.getTokenValue(); | ||
scanner.next(); | ||
} | ||
} | ||
} else { | ||
if (prefix === '' || prefix === '-' || prefix === '+') { // no number | ||
scanner.error('Number or identifier is expected'); | ||
} | ||
b = prefix; | ||
ast = this.context[context].call(this); | ||
} | ||
query = { | ||
type: 'An+B', | ||
loc: getLocation(nthStart, scanner.tokenStart), | ||
a: a, | ||
b: b | ||
}; | ||
} | ||
readSC(); | ||
if (allowOfClause && scanner.lookupValue(0, 'of')) { | ||
scanner.next(); | ||
selector = getSelectorList(NESTED); | ||
} | ||
scanner.eat(RIGHTPARENTHESIS); | ||
return { | ||
type: 'PseudoClass', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
children: new List().appendData({ // TODO: add loc | ||
type: 'Nth', | ||
nth: query, | ||
selector: selector | ||
}) | ||
}; | ||
} | ||
function readNumber() { | ||
var number = scanner.getTokenValue(); | ||
scanner.eat(NUMBER); | ||
return number; | ||
} | ||
// '/' | '*' | ',' | ':' | '+' | '-' | ||
function getOperator() { | ||
var start = scanner.tokenStart; | ||
scanner.next(); | ||
return { | ||
type: 'Operator', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: scanner.substrToCursor(start) | ||
}; | ||
} | ||
// 'progid:' ws* 'DXImageTransform.Microsoft.' ident ws* '(' .* ')' | ||
function checkProgid() { | ||
var startOffset = findNonSCOffset(0); | ||
var offset = startOffset; | ||
if (scanner.lookupValue(offset, 'alpha') || | ||
scanner.lookupValue(offset, 'dropshadow')) { | ||
offset++; | ||
} else { | ||
if (scanner.lookupValue(offset, 'progid') === false || | ||
scanner.lookupType(offset + 1) !== COLON) { | ||
return false; // fail | ||
if (!this.scanner.eof) { | ||
this.scanner.error(); | ||
} | ||
offset += 2; | ||
offset = findNonSCOffset(offset); | ||
if (scanner.lookupValue(offset + 0, 'dximagetransform') === false || | ||
scanner.lookupType(offset + 1) !== FULLSTOP || | ||
scanner.lookupValue(offset + 2, 'microsoft') === false || | ||
scanner.lookupType(offset + 3) !== FULLSTOP || | ||
scanner.lookupType(offset + 4) !== IDENTIFIER) { | ||
return false; // fail | ||
} | ||
offset += 5; | ||
offset = findNonSCOffset(offset); | ||
// console.log(JSON.stringify(ast, null, 4)); | ||
return ast; | ||
} | ||
}; | ||
if (scanner.lookupType(offset) !== LEFTPARENTHESIS) { | ||
return false; // fail | ||
} | ||
var parser = new Parser(); | ||
for (var type; type = scanner.lookupType(offset); offset++) { | ||
if (type === RIGHTPARENTHESIS) { | ||
return offset - startOffset + 1; | ||
} | ||
} | ||
return false; | ||
} | ||
function getProgid(progidEnd) { | ||
var start = scanner.tokenStart; | ||
scanner.skip(progidEnd); | ||
return { | ||
type: 'Progid', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: scanner.substrToCursor(start) | ||
}; | ||
} | ||
// <pseudo-element> | <nth-selector> | <pseudo-class> | ||
function getPseudo() { | ||
var nextType = scanner.lookupType(1); | ||
if (nextType === COLON) { | ||
return getPseudoElement(); | ||
} | ||
if (nextType === IDENTIFIER) { | ||
// '::' starts a pseudo-element, ':' a pseudo-class | ||
// Exceptions: :first-line, :first-letter, :before and :after | ||
if (scanner.lookupValue(1, 'before') || | ||
scanner.lookupValue(1, 'after') || | ||
scanner.lookupValue(1, 'first-letter') || | ||
scanner.lookupValue(1, 'first-line')) { | ||
return getLegacyPseudoElement(); | ||
} | ||
if (scanner.lookupValue(1, 'nth-child') || | ||
scanner.lookupValue(1, 'nth-last-child')) { | ||
return getNthSelector(true); | ||
} | ||
if (scanner.lookupValue(1, 'nth-of-type') || | ||
scanner.lookupValue(1, 'nth-last-of-type')) { | ||
return getNthSelector(false); | ||
} | ||
} | ||
return getPseudoClass(); | ||
} | ||
// :: ident | ||
function getPseudoElement() { | ||
var start = scanner.tokenStart; | ||
var name; | ||
var children = null; | ||
scanner.eat(COLON); | ||
scanner.eat(COLON); | ||
// https://drafts.csswg.org/css-scoping/#slotted-pseudo | ||
if (scanner.lookupValue(0, 'slotted')) { | ||
name = readIdent(false); | ||
scanner.eat(LEFTPARENTHESIS); | ||
children = new List().appendData(getCompoundSelector(true)); | ||
scanner.eat(RIGHTPARENTHESIS); | ||
} else { | ||
name = readIdent(false); | ||
} | ||
return { | ||
type: 'PseudoElement', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
children: children, | ||
legacy: false | ||
}; | ||
} | ||
// : ident | ||
// https://drafts.csswg.org/selectors-4/#grammar | ||
// Some older pseudo-elements (::before, ::after, ::first-line, and ::first-letter) | ||
// can, for legacy reasons, be written using the <pseudo-class-selector> grammar, | ||
// with only a single ":" character at their start. | ||
function getLegacyPseudoElement() { | ||
var start = scanner.tokenStart; | ||
var name; | ||
scanner.eat(COLON); | ||
name = readIdent(false); | ||
return { | ||
type: 'PseudoElement', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: name, | ||
children: null, | ||
legacy: true | ||
}; | ||
} | ||
// : ( ident | function ) | ||
function getPseudoClass() { | ||
var start = scanner.tokenStart; | ||
scanner.eat(COLON); | ||
scanIdent(false); | ||
if (scanner.tokenType === LEFTPARENTHESIS) { | ||
return getFunction(SCOPE_SELECTOR, start, scanner.substrToCursor(start + 1)); | ||
} | ||
return { | ||
type: 'PseudoClass', | ||
loc: getLocation(start, scanner.tokenStart), | ||
name: scanner.substrToCursor(start + 1), | ||
children: null | ||
}; | ||
} | ||
function findNonSCOffset(offset) { | ||
for (var type; type = scanner.lookupType(offset); offset++) { | ||
if (type !== WHITESPACE && type !== COMMENT) { | ||
break; | ||
} | ||
} | ||
return offset; | ||
} | ||
function readSC() { | ||
while (scanner.tokenType === WHITESPACE || scanner.tokenType === COMMENT) { | ||
scanner.next(); | ||
} | ||
} | ||
// node: String | ||
function getString() { | ||
var start = scanner.tokenStart; | ||
scanner.next(); | ||
return { | ||
type: 'String', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: scanner.substrToCursor(start) | ||
}; | ||
} | ||
// # ident | ||
function getHash() { | ||
var start = scanner.tokenStart; | ||
scanner.eat(NUMBERSIGN); | ||
scan: | ||
switch (scanner.tokenType) { | ||
case NUMBER: | ||
if (!isNumber(scanner.source.charCodeAt(scanner.tokenStart))) { | ||
scanner.error('Unexpected input', scanner.tokenStart); | ||
} | ||
for (var pos = scanner.tokenStart + 1; pos < scanner.tokenEnd; pos++) { | ||
var code = scanner.source.charCodeAt(pos); | ||
// break on fullstop or hyperminus/plussign after exponent | ||
if (code === FULLSTOP || code === HYPHENMINUS || code === PLUSSIGN) { | ||
// break token, exclude symbol | ||
scanner.tokenStart = pos; | ||
break scan; | ||
} | ||
} | ||
// number contains digits only, go to next token | ||
scanner.next(); | ||
// if next token is identifier add it to result | ||
if (scanner.tokenType === IDENTIFIER) { | ||
scanner.next(); | ||
} | ||
break; | ||
case IDENTIFIER: | ||
scanner.next(); // add token to result | ||
break; | ||
default: | ||
scanner.error('Number or identifier is expected'); | ||
} | ||
return { | ||
type: 'Hash', | ||
loc: getLocation(start, scanner.tokenStart), | ||
value: scanner.substrToCursor(start + 1) // skip # | ||
}; | ||
} | ||
function parse(source, options) { | ||
var ast; | ||
if (!options || typeof options !== 'object') { | ||
options = {}; | ||
} | ||
var context = options.context || 'stylesheet'; | ||
needPositions = Boolean(options.positions); | ||
filename = options.filename || '<unknown>'; | ||
if (!CONTEXT.hasOwnProperty(context)) { | ||
throw new Error('Unknown context `' + context + '`'); | ||
} | ||
scanner.setSource(source, options.line, options.column); | ||
if (context === 'value') { | ||
ast = getValue(false, options.property ? String(options.property) : null); | ||
} else { | ||
ast = CONTEXT[context](); | ||
} | ||
// console.log(JSON.stringify(ast, null, 4)); | ||
return ast; | ||
}; | ||
// warm up parse to elimitate code branches that never execute | ||
// fix soft deoptimizations (insufficient type feedback) | ||
parse('a.b#c:e:Not(a):Nth-child(2n+1)::g,* b >c+d~e/deep/f,100%{v:1 2em t a(2%, var(--a)) url(..) -foo-bar !important}'); | ||
parser.parse('a.b#c:e:Not(a/**/):AFTER:Nth-child(2n+1)::g::slotted(a/**/),* b >c+d~e/deep/f,100%{v:U+123 1 2em t a(2%, var(--a)) -b() url(..) -foo-bar !important}'); | ||
module.exports = parse; | ||
module.exports = parser.parse.bind(parser); |
@@ -192,2 +192,15 @@ 'use strict'; | ||
}, | ||
lookupNonWSType: function(offset) { | ||
offset += this.currentToken; | ||
for (var type; offset < this.tokenCount; offset++) { | ||
type = this.offsetAndType[offset] >> TYPE_OFFSET; | ||
if (type !== WHITESPACE) { | ||
return type; | ||
} | ||
} | ||
return NULL; | ||
}, | ||
lookupValue: function(offset, referenceStr) { | ||
@@ -215,2 +228,13 @@ offset += this.currentToken; | ||
skipWS: function() { | ||
for (var i = this.currentToken, skipTokenCount = 0; i < this.tokenCount; i++, skipTokenCount++) { | ||
if ((this.offsetAndType[i] >> TYPE_OFFSET) !== WHITESPACE) { | ||
break; | ||
} | ||
} | ||
if (skipTokenCount > 0) { | ||
this.skip(skipTokenCount); | ||
} | ||
}, | ||
skip: function(tokenCount) { | ||
@@ -254,2 +278,20 @@ var next = this.currentToken + tokenCount; | ||
}, | ||
eatNonWS: function(tokenType) { | ||
this.skipWS(); | ||
this.eat(tokenType); | ||
}, | ||
consume: function(tokenType) { | ||
var start = this.tokenStart; | ||
this.eat(tokenType); | ||
return this.substrToCursor(start); | ||
}, | ||
consumeNonWS: function(tokenType) { | ||
this.skipWS(); | ||
return this.consume(tokenType); | ||
}, | ||
expectIdentifier: function(name) { | ||
@@ -331,4 +373,4 @@ if (this.tokenType !== IDENTIFIER || cmpStr(this.source, this.tokenStart, this.tokenEnd, name) === false) { | ||
// fix soft deoptimizations (insufficient type feedback) | ||
new Scanner('\n\r\r\n\f//""\'\'/*\r\n\f*/1a;.\\31\t\+2{url(a);+1.2e3 -.4e-5 .6e+7}'); | ||
new Scanner('\n\r\r\n\f//""\'\'/*\r\n\f*/1a;.\\31\t\+2{url(a);+1.2e3 -.4e-5 .6e+7}').getLocation(); | ||
module.exports = Scanner; |
@@ -64,3 +64,3 @@ 'use strict'; | ||
array[i] = /[a-zA-Z0-9\-]/.test(String.fromCharCode(i)) ? 1 : 0; | ||
}; | ||
} | ||
return array; | ||
@@ -67,0 +67,0 @@ })(); |
@@ -158,6 +158,2 @@ 'use strict'; | ||
lastMatchError: null, | ||
match: function(propertyName, value) { | ||
console.warn('Syntax#match() method is deprecated. Please, use Syntax#matchProperty() or Syntax#matchType() instead'); | ||
return this.matchProperty(propertyName, value); | ||
}, | ||
matchProperty: function(propertyName, value) { | ||
@@ -164,0 +160,0 @@ var property = names.property(propertyName); |
@@ -26,2 +26,3 @@ 'use strict'; | ||
var cursors = null; | ||
var List = function() { | ||
@@ -97,9 +98,36 @@ this.cursor = null; | ||
function allocateCursor(node, prev, next) { | ||
var cursor; | ||
if (cursors !== null) { | ||
cursor = cursors; | ||
cursors = cursors.cursor; | ||
cursor.prev = prev; | ||
cursor.next = next; | ||
cursor.cursor = node.cursor; | ||
} else { | ||
cursor = { | ||
prev: prev, | ||
next: next, | ||
cursor: node.cursor | ||
}; | ||
} | ||
node.cursor = cursor; | ||
return cursor; | ||
} | ||
function releaseCursor(node) { | ||
var cursor = node.cursor; | ||
node.cursor = cursor.cursor; | ||
cursor.prev = null; | ||
cursor.next = null; | ||
cursor.cursor = cursors; | ||
cursors = cursor; | ||
} | ||
List.prototype.each = function(fn, context) { | ||
var item; | ||
var cursor = { | ||
prev: null, | ||
next: this.head, | ||
cursor: this.cursor | ||
}; | ||
@@ -111,3 +139,3 @@ if (context === undefined) { | ||
// push cursor | ||
this.cursor = cursor; | ||
var cursor = allocateCursor(this, null, this.head); | ||
@@ -122,3 +150,3 @@ while (cursor.next !== null) { | ||
// pop cursor | ||
this.cursor = this.cursor.cursor; | ||
releaseCursor(this); | ||
}; | ||
@@ -128,7 +156,2 @@ | ||
var item; | ||
var cursor = { | ||
prev: this.tail, | ||
next: null, | ||
cursor: this.cursor | ||
}; | ||
@@ -140,3 +163,3 @@ if (context === undefined) { | ||
// push cursor | ||
this.cursor = cursor; | ||
var cursor = allocateCursor(this, this.tail, null); | ||
@@ -151,3 +174,3 @@ while (cursor.prev !== null) { | ||
// pop cursor | ||
this.cursor = this.cursor.cursor; | ||
releaseCursor(this); | ||
}; | ||
@@ -161,7 +184,2 @@ | ||
var item; | ||
var cursor = { | ||
prev: null, | ||
next: start, | ||
cursor: this.cursor | ||
}; | ||
@@ -173,3 +191,3 @@ if (context === undefined) { | ||
// push cursor | ||
this.cursor = cursor; | ||
var cursor = allocateCursor(this, null, start); | ||
@@ -186,3 +204,3 @@ while (cursor.next !== null) { | ||
// pop cursor | ||
this.cursor = this.cursor.cursor; | ||
releaseCursor(this); | ||
}; | ||
@@ -196,7 +214,2 @@ | ||
var item; | ||
var cursor = { | ||
prev: start, | ||
next: null, | ||
cursor: this.cursor | ||
}; | ||
@@ -208,3 +221,3 @@ if (context === undefined) { | ||
// push cursor | ||
this.cursor = cursor; | ||
var cursor = allocateCursor(this, start, null); | ||
@@ -221,3 +234,3 @@ while (cursor.prev !== null) { | ||
// pop cursor | ||
this.cursor = this.cursor.cursor; | ||
releaseCursor(this); | ||
}; | ||
@@ -224,0 +237,0 @@ |
@@ -55,3 +55,3 @@ 'use strict'; | ||
if (node.block) { | ||
result += '{' + translate(node.block) + '}'; | ||
result += translate(node.block); | ||
} else { | ||
@@ -89,2 +89,25 @@ result += ';'; | ||
function translateBlock(list) { | ||
var cursor = list.head; | ||
var result = ''; | ||
if (cursor === null) { | ||
return result; | ||
} | ||
if (cursor === list.tail) { | ||
return translate(list.head.data); | ||
} | ||
while (cursor !== null) { | ||
result += translate(cursor.data); | ||
if (cursor.next && cursor.data.type === 'Declaration') { | ||
result += ';'; | ||
} | ||
cursor = cursor.next; | ||
} | ||
return result; | ||
} | ||
function translate(node) { | ||
@@ -99,3 +122,3 @@ switch (node.type) { | ||
case 'Rule': | ||
return translate(node.selector) + '{' + translate(node.block) + '}'; | ||
return translate(node.selector) + translate(node.block); | ||
@@ -109,4 +132,7 @@ case 'SelectorList': | ||
case 'Block': | ||
return eachDelim(node.children, ';'); | ||
return '{' + translateBlock(node.children) + '}'; | ||
case 'DeclarationList': | ||
return translateBlock(node.children); | ||
case 'Declaration': | ||
@@ -162,2 +188,13 @@ return node.important | ||
case 'MediaQueryList': | ||
return eachDelim(node.children, ','); | ||
case 'MediaQuery': | ||
return eachDelim(node.children, ' '); | ||
case 'MediaFeature': | ||
return node.value !== null | ||
? '(' + node.name + ':' + translate(node.value) + ')' | ||
: '(' + node.name + ')'; | ||
case 'Url': | ||
@@ -187,7 +224,5 @@ return 'url(' + translate(node.value) + ')'; | ||
case 'PseudoElement': | ||
return node.legacy | ||
? ':' + node.name // :before, :after, :first-letter and :first-line | ||
: node.children !== null | ||
? '::' + node.name + '(' + each(node.children) + ')' | ||
: '::' + node.name; | ||
return node.children !== null | ||
? '::' + node.name + '(' + each(node.children) + ')' | ||
: '::' + node.name; | ||
@@ -242,2 +277,5 @@ case 'Class': | ||
case 'Ratio': | ||
return node.left + '/' + node.right; | ||
case 'Raw': | ||
@@ -244,0 +282,0 @@ return node.value; |
@@ -43,4 +43,3 @@ 'use strict'; | ||
walk(root, function(chunk, original) { | ||
if (original.line !== null && | ||
original.column !== null) { | ||
if (original.line !== null && original.column !== null) { | ||
if (lastOriginalLine !== original.line || | ||
@@ -64,4 +63,4 @@ lastOriginalColumn !== original.column) { | ||
css += chunk; | ||
lastIndexOfNewline = chunk.lastIndexOf('\n'); | ||
lastIndexOfNewline = chunk.lastIndexOf('\n'); | ||
if (lastIndexOfNewline !== -1) { | ||
@@ -121,2 +120,17 @@ generated.line += chunk.match(/\n/g).length; | ||
function translateBlock(list) { | ||
var cursor = list.head; | ||
var result = []; | ||
while (cursor !== null) { | ||
result.push(translate(cursor.data)); | ||
if (cursor.next && cursor.data.type === 'Declaration') { | ||
result.push(';'); | ||
} | ||
cursor = cursor.next; | ||
} | ||
return result; | ||
} | ||
function translate(node) { | ||
@@ -135,3 +149,3 @@ switch (node.type) { | ||
if (node.block) { | ||
nodes.push('{', translate(node.block), '}'); | ||
nodes.push(translate(node.block)); | ||
} else { | ||
@@ -145,3 +159,3 @@ nodes.push(';'); | ||
return createAnonymousSourceNode([ | ||
translate(node.selector), '{', translate(node.block), '}' | ||
translate(node.selector), translate(node.block) | ||
]); | ||
@@ -165,16 +179,16 @@ | ||
case 'Block': | ||
return createAnonymousSourceNode(node.children.map(translate)).join(';'); | ||
return createAnonymousSourceNode([ | ||
'{', translateBlock(node.children), '}' | ||
]); | ||
case 'DeclarationList': | ||
return translateBlock(node.children); | ||
case 'Declaration': | ||
if (node.important) { | ||
return createSourceNode( | ||
node.loc, | ||
[node.property, ':', translate(node.value), '!important'] | ||
); | ||
} else { | ||
return createSourceNode( | ||
node.loc, | ||
[node.property, ':', translate(node.value)] | ||
); | ||
} | ||
return createSourceNode( | ||
node.loc, | ||
node.important | ||
? [node.property, ':', translate(node.value), '!important'] | ||
: [node.property, ':', translate(node.value)] | ||
); | ||
@@ -186,5 +200,7 @@ case 'Value': | ||
var nodes = [translate(node.nth)]; | ||
if (node.selector !== null) { | ||
nodes.push(' of ', translate(node.selector)); | ||
} | ||
return createAnonymousSourceNode(nodes); | ||
@@ -227,2 +243,13 @@ | ||
case 'MediaQueryList': | ||
return createAnonymousSourceNode(node.children.map(translate)).join(','); | ||
case 'MediaQuery': | ||
return createAnonymousSourceNode(node.children.map(translate)).join(' '); | ||
case 'MediaFeature': | ||
return node.value !== null | ||
? '(' + node.name + ':' + translate(node.value) + ')' | ||
: '(' + node.name + ')'; | ||
case 'Url': | ||
@@ -252,7 +279,5 @@ return 'url(' + translate(node.value) + ')'; | ||
case 'PseudoElement': | ||
return node.legacy | ||
? ':' + node.name // :before, :after, :first-letter and :first-line | ||
: node.children !== null | ||
? '::' + node.name + '(' + each(node.children) + ')' | ||
: '::' + node.name; | ||
return node.children !== null | ||
? '::' + node.name + '(' + each(node.children) + ')' | ||
: '::' + node.name; | ||
@@ -307,2 +332,5 @@ case 'Class': | ||
case 'Ratio': | ||
return node.left + '/' + node.right; | ||
case 'Raw': | ||
@@ -309,0 +337,0 @@ return node.value; |
@@ -28,6 +28,15 @@ 'use strict'; | ||
node.block.children.each(walkRules, this); | ||
walkRules.call(this, node.block); | ||
this.rule = oldRule; | ||
break; | ||
case 'Block': | ||
var oldBlock = this.block; | ||
this.block = node; | ||
node.children.each(walkRules, this); | ||
this.block = oldBlock; | ||
break; | ||
} | ||
@@ -60,3 +69,3 @@ | ||
node.block.children.eachRight(walkRulesRight, this); | ||
walkRulesRight.call(this, node.block); | ||
@@ -67,2 +76,11 @@ this.rule = oldRule; | ||
break; | ||
case 'Block': | ||
var oldBlock = this.block; | ||
this.block = node; | ||
node.children.eachRight(walkRulesRight, this); | ||
this.block = oldBlock; | ||
break; | ||
} | ||
@@ -161,2 +179,6 @@ } | ||
case 'Selector': | ||
node.children.each(walk, this); | ||
break; | ||
case 'Nth': | ||
@@ -170,5 +192,14 @@ walk.call(this, node.nth); | ||
case 'Block': | ||
var oldBlock = this.block; | ||
this.block = node; | ||
node.children.each(walk, this); | ||
this.block = oldBlock; | ||
break; | ||
case 'DeclarationList': | ||
node.children.each(walk, this); | ||
break; | ||
case 'Declaration': | ||
@@ -225,5 +256,24 @@ this.declaration = node; | ||
case 'Selector': | ||
case 'MediaQueryList': | ||
node.children.each(walk, this); | ||
break; | ||
case 'MediaQuery': | ||
node.children.each(walk, this); | ||
break; | ||
case 'MediaFeature': | ||
if (node.value !== null) { | ||
walk.call(this, node.value); | ||
} | ||
break; | ||
case 'Value': | ||
node.children.each(walk, this); | ||
break; | ||
case 'Parentheses': | ||
node.children.each(walk, this); | ||
break; | ||
case 'Brackets': | ||
@@ -256,2 +306,3 @@ node.children.each(walk, this); | ||
// case 'Raw': | ||
// case 'Ratio': | ||
} | ||
@@ -268,2 +319,3 @@ } | ||
selector: null, | ||
block: null, | ||
declaration: null, | ||
@@ -270,0 +322,0 @@ function: null |
{ | ||
"name": "css-tree", | ||
"version": "1.0.0-alpha13", | ||
"version": "1.0.0-alpha14", | ||
"description": "Fast detailed CSS parser", | ||
@@ -37,3 +37,9 @@ "keywords": [ | ||
"no-undef": 2, | ||
"no-unused-vars": [2, {"vars": "all", "args": "after-used"}] | ||
"no-unused-vars": [ | ||
2, | ||
{ | ||
"vars": "all", | ||
"args": "after-used" | ||
} | ||
] | ||
} | ||
@@ -46,7 +52,7 @@ }, | ||
"codestyle": "jscs data lib scripts test && eslint data lib scripts test", | ||
"test": "mocha --reporter dot", | ||
"coverage": "istanbul cover _mocha -- -R dot", | ||
"test": "mocha --reporter progress", | ||
"coverage": "istanbul cover _mocha -- -R min", | ||
"prepublish": "npm run build", | ||
"travis": "npm run codestyle-and-test && npm run coveralls", | ||
"coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | coveralls", | ||
"coveralls": "istanbul cover _mocha --report lcovonly -- -R min && cat ./coverage/lcov.info | coveralls", | ||
"hydrogen": "node --trace-hydrogen --trace-phase=Z --trace-deopt --code-comments --hydrogen-track-positions --redirect-code-traces --redirect-code-traces-to=code.asm --trace_hydrogen_file=code.cfg --print-opt-code bin/parse --stat -o /dev/null" | ||
@@ -63,2 +69,3 @@ }, | ||
"jscs": "~3.0.7", | ||
"json-to-ast": "^1.2.15", | ||
"mocha": "^3.0.2", | ||
@@ -65,0 +72,0 @@ "uglify-js": "^2.6.1" |
@@ -13,8 +13,9 @@ <img align="right" width="111" height="111" | ||
Fast detailed CSS parser | ||
[Fast](https://github.com/postcss/benchmark) detailed CSS parser | ||
> Work in progress | ||
> Work in progress. Project in alpha stage since AST format is subject to change. | ||
Docs and tools: | ||
* [AST Explorer](https://astexplorer.net/#/gist/244e2fb4da940df52bf0f4b94277db44/e79aff44611020b22cfd9708f3a99ce09b7d67a8) – explore CSSTree AST format with zero setup | ||
* [CSS syntax reference](https://csstree.github.io/docs/syntax.html) | ||
@@ -69,3 +70,4 @@ * [CSS syntax validator](https://csstree.github.io/docs/validator.html) | ||
- `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) | ||
@@ -81,8 +83,9 @@ - `filename` String – filename of source that adds to info when `positions` is true, uses for source map generation (`<unknown>` by default) | ||
- `atruleExpression` – at-rule expression (`screen, print` for example above) | ||
- `ruleset` – rule (e.g. `.foo, .bar:hover { color: red; border: 1px solid black; }`) | ||
- `selectorList` – selector group (`.foo, .bar:hover` for ruleset example) | ||
- `selector` – selector (`.foo` or `.bar:hover` for ruleset example) | ||
- `block` – block content w/o curly braces (`color: red; border: 1px solid black;` for ruleset example) | ||
- `declaration` – declaration (`color: red` or `border: 1px solid black` for ruleset example) | ||
- `value` – declaration value (`red` or `1px solid black` for ruleset example) | ||
- `rule` – rule (e.g. `.foo, .bar:hover { color: red; border: 1px solid black; }`) | ||
- `selectorList` – selector group (`.foo, .bar:hover` for rule example) | ||
- `selector` – selector (`.foo` or `.bar:hover` for rule example) | ||
- `block` – block with curly braces (`{ color: red; border: 1px solid black; }` for rule example) | ||
- `declarationList` – block content w/o curly braces (`color: red; border: 1px solid black;` for rule example), useful to parse HTML `style` attribute value | ||
- `declaration` – declaration (`color: red` or `border: 1px solid black` for rule example) | ||
- `value` – declaration value (`red` or `1px solid black` for rule example) | ||
@@ -153,9 +156,10 @@ ```js | ||
- `root` – refers to `ast` or root node | ||
- `stylesheet` – refers to closest `StyleSheet` node, it may be a top-level or at-rule block stylesheet | ||
- `atruleExpression` – refers to `AtruleExpression` node if current node inside at-rule expression | ||
- `ruleset` – refers to `Rule` node if current node inside a ruleset | ||
- `selector` – refers to `SelectorList` node if current node inside a selector list | ||
- `declaration` – refers to `Declaration` node if current node inside a declaration | ||
- `function` – refers to closest `Function` or `FunctionalPseudo` node if current node inside one of them | ||
- `root` – refers to `ast` root node (actually it's a node passed to walker function) | ||
- `stylesheet` – refers to `StyleSheet` node, usually it's a root node | ||
- `atruleExpression` – refers to `AtruleExpression` node if any | ||
- `rule` – refers to closest `Rule` node if any | ||
- `selector` – refers to `SelectorList` node if any | ||
- `block` - refers to closest `Block` node if any | ||
- `declaration` – refers to `Declaration` node if any | ||
- `function` – refers to closest `Function`, `PseudoClass` or `PseudoElement` node if current node inside one of them | ||
@@ -162,0 +166,0 @@ ```js |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
639214
82
13062
240
8