12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294 |
- /*!
- * Stylus - Parser
- * Copyright (c) Automattic <developer.wordpress.com>
- * MIT Licensed
- */
- /**
- * Module dependencies.
- */
- var Lexer = require('./lexer')
- , nodes = require('./nodes')
- , Token = require('./token')
- , units = require('./units')
- , errors = require('./errors')
- , cache = require('./cache');
- // debuggers
- var debug = {
- lexer: require('debug')('stylus:lexer')
- , selector: require('debug')('stylus:parser:selector')
- };
- /**
- * Selector composite tokens.
- */
- var selectorTokens = [
- 'ident'
- , 'string'
- , 'selector'
- , 'function'
- , 'comment'
- , 'boolean'
- , 'space'
- , 'color'
- , 'unit'
- , 'for'
- , 'in'
- , '['
- , ']'
- , '('
- , ')'
- , '+'
- , '-'
- , '*'
- , '*='
- , '<'
- , '>'
- , '='
- , ':'
- , '&'
- , '&&'
- , '~'
- , '{'
- , '}'
- , '.'
- , '..'
- , '/'
- ];
- /**
- * CSS pseudo-classes and pseudo-elements.
- * See http://dev.w3.org/csswg/selectors4/
- */
- var pseudoSelectors = [
- // Logical Combinations
- 'matches'
- , 'not'
- // Linguistic Pseudo-classes
- , 'dir'
- , 'lang'
- // Location Pseudo-classes
- , 'any-link'
- , 'link'
- , 'visited'
- , 'local-link'
- , 'target'
- , 'scope'
- // User Action Pseudo-classes
- , 'hover'
- , 'active'
- , 'focus'
- , 'drop'
- // Time-dimensional Pseudo-classes
- , 'current'
- , 'past'
- , 'future'
- // The Input Pseudo-classes
- , 'enabled'
- , 'disabled'
- , 'read-only'
- , 'read-write'
- , 'placeholder-shown'
- , 'checked'
- , 'indeterminate'
- , 'valid'
- , 'invalid'
- , 'in-range'
- , 'out-of-range'
- , 'required'
- , 'optional'
- , 'user-error'
- // Tree-Structural pseudo-classes
- , 'root'
- , 'empty'
- , 'blank'
- , 'nth-child'
- , 'nth-last-child'
- , 'first-child'
- , 'last-child'
- , 'only-child'
- , 'nth-of-type'
- , 'nth-last-of-type'
- , 'first-of-type'
- , 'last-of-type'
- , 'only-of-type'
- , 'nth-match'
- , 'nth-last-match'
- // Grid-Structural Selectors
- , 'nth-column'
- , 'nth-last-column'
- // Pseudo-elements
- , 'first-line'
- , 'first-letter'
- , 'before'
- , 'after'
- // Non-standard
- , 'selection'
- ];
- /**
- * Initialize a new `Parser` with the given `str` and `options`.
- *
- * @param {String} str
- * @param {Object} options
- * @api private
- */
- var Parser = module.exports = function Parser(str, options) {
- var self = this;
- options = options || {};
- Parser.cache = Parser.cache || Parser.getCache(options);
- this.hash = Parser.cache.key(str, options);
- this.lexer = {};
- if (!Parser.cache.has(this.hash)) {
- this.lexer = new Lexer(str, options);
- }
- this.prefix = options.prefix || '';
- this.root = options.root || new nodes.Root;
- this.state = ['root'];
- this.stash = [];
- this.parens = 0;
- this.css = 0;
- this.state.pop = function(){
- self.prevState = [].pop.call(this);
- };
- };
- /**
- * Get cache instance.
- *
- * @param {Object} options
- * @return {Object}
- * @api private
- */
- Parser.getCache = function(options) {
- return false === options.cache
- ? cache(false)
- : cache(options.cache || 'memory', options);
- };
- /**
- * Parser prototype.
- */
- Parser.prototype = {
- /**
- * Constructor.
- */
- constructor: Parser,
- /**
- * Return current state.
- *
- * @return {String}
- * @api private
- */
- currentState: function() {
- return this.state[this.state.length - 1];
- },
- /**
- * Return previous state.
- *
- * @return {String}
- * @api private
- */
- previousState: function() {
- return this.state[this.state.length - 2];
- },
- /**
- * Parse the input, then return the root node.
- *
- * @return {Node}
- * @api private
- */
- parse: function(){
- var block = this.parent = this.root;
- if (Parser.cache.has(this.hash)) {
- block = Parser.cache.get(this.hash);
- // normalize cached imports
- if ('block' == block.nodeName) block.constructor = nodes.Root;
- } else {
- while ('eos' != this.peek().type) {
- this.skipWhitespace();
- if ('eos' == this.peek().type) break;
- var stmt = this.statement();
- this.accept(';');
- if (!stmt) this.error('unexpected token {peek}, not allowed at the root level');
- block.push(stmt);
- }
- Parser.cache.set(this.hash, block);
- }
- return block;
- },
- /**
- * Throw an `Error` with the given `msg`.
- *
- * @param {String} msg
- * @api private
- */
- error: function(msg){
- var type = this.peek().type
- , val = undefined == this.peek().val
- ? ''
- : ' ' + this.peek().toString();
- if (val.trim() == type.trim()) val = '';
- throw new errors.ParseError(msg.replace('{peek}', '"' + type + val + '"'));
- },
- /**
- * Accept the given token `type`, and return it,
- * otherwise return `undefined`.
- *
- * @param {String} type
- * @return {Token}
- * @api private
- */
- accept: function(type){
- if (type == this.peek().type) {
- return this.next();
- }
- },
- /**
- * Expect token `type` and return it, throw otherwise.
- *
- * @param {String} type
- * @return {Token}
- * @api private
- */
- expect: function(type){
- if (type != this.peek().type) {
- this.error('expected "' + type + '", got {peek}');
- }
- return this.next();
- },
- /**
- * Get the next token.
- *
- * @return {Token}
- * @api private
- */
- next: function() {
- var tok = this.stash.length
- ? this.stash.pop()
- : this.lexer.next()
- , line = tok.lineno
- , column = tok.column || 1;
- if (tok.val && tok.val.nodeName) {
- tok.val.lineno = line;
- tok.val.column = column;
- }
- nodes.lineno = line;
- nodes.column = column;
- debug.lexer('%s %s', tok.type, tok.val || '');
- return tok;
- },
- /**
- * Peek with lookahead(1).
- *
- * @return {Token}
- * @api private
- */
- peek: function() {
- return this.lexer.peek();
- },
- /**
- * Lookahead `n` tokens.
- *
- * @param {Number} n
- * @return {Token}
- * @api private
- */
- lookahead: function(n){
- return this.lexer.lookahead(n);
- },
- /**
- * Check if the token at `n` is a valid selector token.
- *
- * @param {Number} n
- * @return {Boolean}
- * @api private
- */
- isSelectorToken: function(n) {
- var la = this.lookahead(n).type;
- switch (la) {
- case 'for':
- return this.bracketed;
- case '[':
- this.bracketed = true;
- return true;
- case ']':
- this.bracketed = false;
- return true;
- default:
- return ~selectorTokens.indexOf(la);
- }
- },
- /**
- * Check if the token at `n` is a pseudo selector.
- *
- * @param {Number} n
- * @return {Boolean}
- * @api private
- */
- isPseudoSelector: function(n){
- var val = this.lookahead(n).val;
- return val && ~pseudoSelectors.indexOf(val.name);
- },
- /**
- * Check if the current line contains `type`.
- *
- * @param {String} type
- * @return {Boolean}
- * @api private
- */
- lineContains: function(type){
- var i = 1
- , la;
- while (la = this.lookahead(i++)) {
- if (~['indent', 'outdent', 'newline', 'eos'].indexOf(la.type)) return;
- if (type == la.type) return true;
- }
- },
- /**
- * Valid selector tokens.
- */
- selectorToken: function() {
- if (this.isSelectorToken(1)) {
- if ('{' == this.peek().type) {
- // unclosed, must be a block
- if (!this.lineContains('}')) return;
- // check if ':' is within the braces.
- // though not required by Stylus, chances
- // are if someone is using {} they will
- // use CSS-style props, helping us with
- // the ambiguity in this case
- var i = 0
- , la;
- while (la = this.lookahead(++i)) {
- if ('}' == la.type) {
- // Check empty block.
- if (i == 2 || (i == 3 && this.lookahead(i - 1).type == 'space'))
- return;
- break;
- }
- if (':' == la.type) return;
- }
- }
- return this.next();
- }
- },
- /**
- * Skip the given `tokens`.
- *
- * @param {Array} tokens
- * @api private
- */
- skip: function(tokens) {
- while (~tokens.indexOf(this.peek().type))
- this.next();
- },
- /**
- * Consume whitespace.
- */
- skipWhitespace: function() {
- this.skip(['space', 'indent', 'outdent', 'newline']);
- },
- /**
- * Consume newlines.
- */
- skipNewlines: function() {
- while ('newline' == this.peek().type)
- this.next();
- },
- /**
- * Consume spaces.
- */
- skipSpaces: function() {
- while ('space' == this.peek().type)
- this.next();
- },
- /**
- * Consume spaces and comments.
- */
- skipSpacesAndComments: function() {
- while ('space' == this.peek().type
- || 'comment' == this.peek().type)
- this.next();
- },
- /**
- * Check if the following sequence of tokens
- * forms a function definition, ie trailing
- * `{` or indentation.
- */
- looksLikeFunctionDefinition: function(i) {
- return 'indent' == this.lookahead(i).type
- || '{' == this.lookahead(i).type;
- },
- /**
- * Check if the following sequence of tokens
- * forms a selector.
- *
- * @param {Boolean} [fromProperty]
- * @return {Boolean}
- * @api private
- */
- looksLikeSelector: function(fromProperty) {
- var i = 1
- , node
- , brace;
- // Real property
- if (fromProperty && ':' == this.lookahead(i + 1).type
- && (this.lookahead(i + 1).space || 'indent' == this.lookahead(i + 2).type))
- return false;
- // Assume selector when an ident is
- // followed by a selector
- while ('ident' == this.lookahead(i).type
- && ('newline' == this.lookahead(i + 1).type
- || ',' == this.lookahead(i + 1).type)) i += 2;
- while (this.isSelectorToken(i)
- || ',' == this.lookahead(i).type) {
- if ('selector' == this.lookahead(i).type)
- return true;
- if ('&' == this.lookahead(i + 1).type)
- return true;
- // Hash values inside properties
- if (
- i > 1 &&
- 'ident' === this.lookahead(i - 1).type &&
- '.' === this.lookahead(i).type &&
- 'ident' === this.lookahead(i + 1).type
- ) {
- while ((node = this.lookahead(i + 2))) {
- if ([
- 'indent',
- 'outdent',
- '{',
- ';',
- 'eos',
- 'selector',
- 'media',
- 'if',
- 'atrule',
- ')',
- '}',
- 'unit',
- '[',
- 'for',
- 'function'
- ].indexOf(node.type) !== -1) {
- if (node.type === '[') {
- while ((node = this.lookahead(i + 3)) && node.type !== ']') {
- if (~['.', 'unit'].indexOf(node.type)) {
- return false;
- }
- i += 1
- }
- } else {
- if (this.isPseudoSelector(i + 2)) {
- return true;
- }
- if (node.type === ')' && this.lookahead(i + 3) && this.lookahead(i + 3).type === '}') {
- break;
- }
- return [
- 'outdent',
- ';',
- 'eos',
- 'media',
- 'if',
- 'atrule',
- ')',
- '}',
- 'unit',
- 'for',
- 'function'
- ].indexOf(node.type) === -1;
- }
- }
- i += 1
- }
- return true;
- }
- if ('.' == this.lookahead(i).type && 'ident' == this.lookahead(i + 1).type) {
- return true;
- }
- if ('*' == this.lookahead(i).type && 'newline' == this.lookahead(i + 1).type)
- return true;
- // Pseudo-elements
- if (':' == this.lookahead(i).type
- && ':' == this.lookahead(i + 1).type)
- return true;
- // #a after an ident and newline
- if ('color' == this.lookahead(i).type
- && 'newline' == this.lookahead(i - 1).type)
- return true;
- if (this.looksLikeAttributeSelector(i))
- return true;
- if (('=' == this.lookahead(i).type || 'function' == this.lookahead(i).type)
- && '{' == this.lookahead(i + 1).type)
- return false;
- // Hash values inside properties
- if (':' == this.lookahead(i).type
- && !this.isPseudoSelector(i + 1)
- && this.lineContains('.'))
- return false;
- // the ':' token within braces signifies
- // a selector. ex: "foo{bar:'baz'}"
- if ('{' == this.lookahead(i).type) brace = true;
- else if ('}' == this.lookahead(i).type) brace = false;
- if (brace && ':' == this.lookahead(i).type) return true;
- // '{' preceded by a space is considered a selector.
- // for example "foo{bar}{baz}" may be a property,
- // however "foo{bar} {baz}" is a selector
- if ('space' == this.lookahead(i).type
- && '{' == this.lookahead(i + 1).type)
- return true;
- // Assume pseudo selectors are NOT properties
- // as 'td:th-child(1)' may look like a property
- // and function call to the parser otherwise
- if (':' == this.lookahead(i++).type
- && !this.lookahead(i-1).space
- && this.isPseudoSelector(i))
- return true;
- // Trailing space
- if ('space' == this.lookahead(i).type
- && 'newline' == this.lookahead(i + 1).type
- && '{' == this.lookahead(i + 2).type)
- return true;
- if (',' == this.lookahead(i).type
- && 'newline' == this.lookahead(i + 1).type)
- return true;
- }
- // Trailing comma
- if (',' == this.lookahead(i).type
- && 'newline' == this.lookahead(i + 1).type)
- return true;
- // Trailing brace
- if ('{' == this.lookahead(i).type
- && 'newline' == this.lookahead(i + 1).type)
- return true;
- // css-style mode, false on ; }
- if (this.css) {
- if (';' == this.lookahead(i).type ||
- '}' == this.lookahead(i - 1).type)
- return false;
- }
- // Trailing separators
- while (!~[
- 'indent'
- , 'outdent'
- , 'newline'
- , 'for'
- , 'if'
- , ';'
- , '}'
- , 'eos'].indexOf(this.lookahead(i).type))
- ++i;
- if ('indent' == this.lookahead(i).type)
- return true;
- },
- /**
- * Check if the following sequence of tokens
- * forms an attribute selector.
- */
- looksLikeAttributeSelector: function(n) {
- var type = this.lookahead(n).type;
- if ('=' == type && this.bracketed) return true;
- return ('ident' == type || 'string' == type)
- && ']' == this.lookahead(n + 1).type
- && ('newline' == this.lookahead(n + 2).type || this.isSelectorToken(n + 2))
- && !this.lineContains(':')
- && !this.lineContains('=');
- },
- /**
- * Check if the following sequence of tokens
- * forms a keyframe block.
- */
- looksLikeKeyframe: function() {
- var i = 2
- , type;
- switch (this.lookahead(i).type) {
- case '{':
- case 'indent':
- case ',':
- return true;
- case 'newline':
- while ('unit' == this.lookahead(++i).type
- || 'newline' == this.lookahead(i).type) ;
- type = this.lookahead(i).type;
- return 'indent' == type || '{' == type;
- }
- },
- /**
- * Check if the current state supports selectors.
- */
- stateAllowsSelector: function() {
- switch (this.currentState()) {
- case 'root':
- case 'atblock':
- case 'selector':
- case 'conditional':
- case 'function':
- case 'atrule':
- case 'for':
- return true;
- }
- },
- /**
- * Try to assign @block to the node.
- *
- * @param {Expression} expr
- * @private
- */
- assignAtblock: function(expr) {
- try {
- expr.push(this.atblock(expr));
- } catch(err) {
- this.error('invalid right-hand side operand in assignment, got {peek}');
- }
- },
- /**
- * statement
- * | statement 'if' expression
- * | statement 'unless' expression
- */
- statement: function() {
- var stmt = this.stmt()
- , state = this.prevState
- , block
- , op;
- // special-case statements since it
- // is not an expression. We could
- // implement postfix conditionals at
- // the expression level, however they
- // would then fail to enclose properties
- if (this.allowPostfix) {
- this.allowPostfix = false;
- state = 'expression';
- }
- switch (state) {
- case 'assignment':
- case 'expression':
- case 'function arguments':
- while (op =
- this.accept('if')
- || this.accept('unless')
- || this.accept('for')) {
- switch (op.type) {
- case 'if':
- case 'unless':
- stmt = new nodes.If(this.expression(), stmt);
- stmt.postfix = true;
- stmt.negate = 'unless' == op.type;
- this.accept(';');
- break;
- case 'for':
- var key
- , val = this.id().name;
- if (this.accept(',')) key = this.id().name;
- this.expect('in');
- var each = new nodes.Each(val, key, this.expression());
- block = new nodes.Block(this.parent, each);
- block.push(stmt);
- each.block = block;
- stmt = each;
- }
- }
- }
- return stmt;
- },
- /**
- * ident
- * | selector
- * | literal
- * | charset
- * | namespace
- * | import
- * | require
- * | media
- * | atrule
- * | scope
- * | keyframes
- * | mozdocument
- * | for
- * | if
- * | unless
- * | comment
- * | expression
- * | 'return' expression
- */
- stmt: function() {
- var tok = this.peek(), selector;
- switch (tok.type) {
- case 'keyframes':
- return this.keyframes();
- case '-moz-document':
- return this.mozdocument();
- case 'comment':
- case 'selector':
- case 'literal':
- case 'charset':
- case 'namespace':
- case 'import':
- case 'require':
- case 'extend':
- case 'media':
- case 'atrule':
- case 'ident':
- case 'scope':
- case 'supports':
- case 'unless':
- case 'function':
- case 'for':
- case 'if':
- return this[tok.type]();
- case 'return':
- return this.return();
- case '{':
- return this.property();
- default:
- // Contextual selectors
- if (this.stateAllowsSelector()) {
- switch (tok.type) {
- case 'color':
- case '~':
- case '>':
- case '<':
- case ':':
- case '&':
- case '&&':
- case '[':
- case '.':
- case '/':
- selector = this.selector();
- selector.column = tok.column;
- selector.lineno = tok.lineno;
- return selector;
- // relative reference
- case '..':
- if ('/' == this.lookahead(2).type)
- return this.selector();
- case '+':
- return 'function' == this.lookahead(2).type
- ? this.functionCall()
- : this.selector();
- case '*':
- return this.property();
- // keyframe blocks (10%, 20% { ... })
- case 'unit':
- if (this.looksLikeKeyframe()) {
- selector = this.selector();
- selector.column = tok.column;
- selector.lineno = tok.lineno;
- return selector;
- }
- case '-':
- if ('{' == this.lookahead(2).type)
- return this.property();
- }
- }
- // Expression fallback
- var expr = this.expression();
- if (expr.isEmpty) this.error('unexpected {peek}');
- return expr;
- }
- },
- /**
- * indent (!outdent)+ outdent
- */
- block: function(node, scope) {
- var delim
- , stmt
- , next
- , block = this.parent = new nodes.Block(this.parent, node);
- if (false === scope) block.scope = false;
- this.accept('newline');
- // css-style
- if (this.accept('{')) {
- this.css++;
- delim = '}';
- this.skipWhitespace();
- } else {
- delim = 'outdent';
- this.expect('indent');
- }
- while (delim != this.peek().type) {
- // css-style
- if (this.css) {
- if (this.accept('newline') || this.accept('indent')) continue;
- stmt = this.statement();
- this.accept(';');
- this.skipWhitespace();
- } else {
- if (this.accept('newline')) continue;
- // skip useless indents and comments
- next = this.lookahead(2).type;
- if ('indent' == this.peek().type
- && ~['outdent', 'newline', 'comment'].indexOf(next)) {
- this.skip(['indent', 'outdent']);
- continue;
- }
- if ('eos' == this.peek().type) return block;
- stmt = this.statement();
- this.accept(';');
- }
- if (!stmt) this.error('unexpected token {peek} in block');
- block.push(stmt);
- }
- // css-style
- if (this.css) {
- this.skipWhitespace();
- this.expect('}');
- this.skipSpaces();
- this.css--;
- } else {
- this.expect('outdent');
- }
- this.parent = block.parent;
- return block;
- },
- /**
- * comment space*
- */
- comment: function(){
- var node = this.next().val;
- this.skipSpaces();
- return node;
- },
- /**
- * for val (',' key) in expr
- */
- for: function() {
- this.expect('for');
- var key
- , val = this.id().name;
- if (this.accept(',')) key = this.id().name;
- this.expect('in');
- this.state.push('for');
- this.cond = true;
- var each = new nodes.Each(val, key, this.expression());
- this.cond = false;
- each.block = this.block(each, false);
- this.state.pop();
- return each;
- },
- /**
- * return expression
- */
- return: function() {
- this.expect('return');
- var expr = this.expression();
- return expr.isEmpty
- ? new nodes.Return
- : new nodes.Return(expr);
- },
- /**
- * unless expression block
- */
- unless: function() {
- this.expect('unless');
- this.state.push('conditional');
- this.cond = true;
- var node = new nodes.If(this.expression(), true);
- this.cond = false;
- node.block = this.block(node, false);
- this.state.pop();
- return node;
- },
- /**
- * if expression block (else block)?
- */
- if: function() {
- var token = this.expect('if');
- this.state.push('conditional');
- this.cond = true;
- var node = new nodes.If(this.expression())
- , cond
- , block
- , item;
- node.column = token.column;
- this.cond = false;
- node.block = this.block(node, false);
- this.skip(['newline', 'comment']);
- while (this.accept('else')) {
- token = this.accept('if');
- if (token) {
- this.cond = true;
- cond = this.expression();
- this.cond = false;
- block = this.block(node, false);
- item = new nodes.If(cond, block);
- item.column = token.column;
- node.elses.push(item);
- } else {
- node.elses.push(this.block(node, false));
- break;
- }
- this.skip(['newline', 'comment']);
- }
- this.state.pop();
- return node;
- },
- /**
- * @block
- *
- * @param {Expression} [node]
- */
- atblock: function(node){
- if (!node) this.expect('atblock');
- node = new nodes.Atblock;
- this.state.push('atblock');
- node.block = this.block(node, false);
- this.state.pop();
- return node;
- },
- /**
- * atrule selector? block?
- */
- atrule: function(){
- var type = this.expect('atrule').val
- , node = new nodes.Atrule(type)
- , tok;
- this.skipSpacesAndComments();
- node.segments = this.selectorParts();
- this.skipSpacesAndComments();
- tok = this.peek().type;
- if ('indent' == tok || '{' == tok || ('newline' == tok
- && '{' == this.lookahead(2).type)) {
- this.state.push('atrule');
- node.block = this.block(node);
- this.state.pop();
- }
- return node;
- },
- /**
- * scope
- */
- scope: function(){
- this.expect('scope');
- var selector = this.selectorParts()
- .map(function(selector) { return selector.val; })
- .join('');
- this.selectorScope = selector.trim();
- return nodes.null;
- },
- /**
- * supports
- */
- supports: function(){
- this.expect('supports');
- var node = new nodes.Supports(this.supportsCondition());
- this.state.push('atrule');
- node.block = this.block(node);
- this.state.pop();
- return node;
- },
- /**
- * supports negation
- * | supports op
- * | expression
- */
- supportsCondition: function(){
- var node = this.supportsNegation()
- || this.supportsOp();
- if (!node) {
- this.cond = true;
- node = this.expression();
- this.cond = false;
- }
- return node;
- },
- /**
- * 'not' supports feature
- */
- supportsNegation: function(){
- if (this.accept('not')) {
- var node = new nodes.Expression;
- node.push(new nodes.Literal('not'));
- node.push(this.supportsFeature());
- return node;
- }
- },
- /**
- * supports feature (('and' | 'or') supports feature)+
- */
- supportsOp: function(){
- var feature = this.supportsFeature()
- , op
- , expr;
- if (feature) {
- expr = new nodes.Expression;
- expr.push(feature);
- while (op = this.accept('&&') || this.accept('||')) {
- expr.push(new nodes.Literal('&&' == op.val ? 'and' : 'or'));
- expr.push(this.supportsFeature());
- }
- return expr;
- }
- },
- /**
- * ('(' supports condition ')')
- * | feature
- */
- supportsFeature: function(){
- this.skipSpacesAndComments();
- if ('(' == this.peek().type) {
- var la = this.lookahead(2).type;
- if ('ident' == la || '{' == la) {
- return this.feature();
- } else {
- this.expect('(');
- var node = new nodes.Expression;
- node.push(new nodes.Literal('('));
- node.push(this.supportsCondition());
- this.expect(')')
- node.push(new nodes.Literal(')'));
- this.skipSpacesAndComments();
- return node;
- }
- }
- },
- /**
- * extend
- */
- extend: function(){
- var tok = this.expect('extend')
- , selectors = []
- , sel
- , node
- , arr;
- do {
- arr = this.selectorParts();
- if (!arr.length) continue;
- sel = new nodes.Selector(arr);
- selectors.push(sel);
- if ('!' !== this.peek().type) continue;
- tok = this.lookahead(2);
- if ('ident' !== tok.type || 'optional' !== tok.val.name) continue;
- this.skip(['!', 'ident']);
- sel.optional = true;
- } while(this.accept(','));
- node = new nodes.Extend(selectors);
- node.lineno = tok.lineno;
- node.column = tok.column;
- return node;
- },
- /**
- * media queries
- */
- media: function() {
- this.expect('media');
- this.state.push('atrule');
- var media = new nodes.Media(this.queries());
- media.block = this.block(media);
- this.state.pop();
- return media;
- },
- /**
- * query (',' query)*
- */
- queries: function() {
- var queries = new nodes.QueryList
- , skip = ['comment', 'newline', 'space'];
- do {
- this.skip(skip);
- queries.push(this.query());
- this.skip(skip);
- } while (this.accept(','));
- return queries;
- },
- /**
- * expression
- * | (ident | 'not')? ident ('and' feature)*
- * | feature ('and' feature)*
- */
- query: function() {
- var query = new nodes.Query
- , expr
- , pred
- , id;
- // hash values support
- if ('ident' == this.peek().type
- && ('.' == this.lookahead(2).type
- || '[' == this.lookahead(2).type)) {
- this.cond = true;
- expr = this.expression();
- this.cond = false;
- query.push(new nodes.Feature(expr.nodes));
- return query;
- }
- if (pred = this.accept('ident') || this.accept('not')) {
- pred = new nodes.Literal(pred.val.string || pred.val);
- this.skipSpacesAndComments();
- if (id = this.accept('ident')) {
- query.type = id.val;
- query.predicate = pred;
- } else {
- query.type = pred;
- }
- this.skipSpacesAndComments();
- if (!this.accept('&&')) return query;
- }
- do {
- query.push(this.feature());
- } while (this.accept('&&'));
- return query;
- },
- /**
- * '(' ident ( ':'? expression )? ')'
- */
- feature: function() {
- this.skipSpacesAndComments();
- this.expect('(');
- this.skipSpacesAndComments();
- var node = new nodes.Feature(this.interpolate());
- this.skipSpacesAndComments();
- this.accept(':')
- this.skipSpacesAndComments();
- this.inProperty = true;
- node.expr = this.list();
- this.inProperty = false;
- this.skipSpacesAndComments();
- this.expect(')');
- this.skipSpacesAndComments();
- return node;
- },
- /**
- * @-moz-document call (',' call)* block
- */
- mozdocument: function(){
- this.expect('-moz-document');
- var mozdocument = new nodes.Atrule('-moz-document')
- , calls = [];
- do {
- this.skipSpacesAndComments();
- calls.push(this.functionCall());
- this.skipSpacesAndComments();
- } while (this.accept(','));
- mozdocument.segments = [new nodes.Literal(calls.join(', '))];
- this.state.push('atrule');
- mozdocument.block = this.block(mozdocument, false);
- this.state.pop();
- return mozdocument;
- },
- /**
- * import expression
- */
- import: function() {
- this.expect('import');
- this.allowPostfix = true;
- return new nodes.Import(this.expression(), false);
- },
- /**
- * require expression
- */
- require: function() {
- this.expect('require');
- this.allowPostfix = true;
- return new nodes.Import(this.expression(), true);
- },
- /**
- * charset string
- */
- charset: function() {
- this.expect('charset');
- var str = this.expect('string').val;
- this.allowPostfix = true;
- return new nodes.Charset(str);
- },
- /**
- * namespace ident? (string | url)
- */
- namespace: function() {
- var str
- , prefix;
- this.expect('namespace');
- this.skipSpacesAndComments();
- if (prefix = this.accept('ident')) {
- prefix = prefix.val;
- }
- this.skipSpacesAndComments();
- str = this.accept('string') || this.url();
- this.allowPostfix = true;
- return new nodes.Namespace(str, prefix);
- },
- /**
- * keyframes name block
- */
- keyframes: function() {
- var tok = this.expect('keyframes')
- , keyframes;
- this.skipSpacesAndComments();
- keyframes = new nodes.Keyframes(this.selectorParts(), tok.val);
- keyframes.column = tok.column;
- this.skipSpacesAndComments();
- // block
- this.state.push('atrule');
- keyframes.block = this.block(keyframes);
- this.state.pop();
- return keyframes;
- },
- /**
- * literal
- */
- literal: function() {
- return this.expect('literal').val;
- },
- /**
- * ident space?
- */
- id: function() {
- var tok = this.expect('ident');
- this.accept('space');
- return tok.val;
- },
- /**
- * ident
- * | assignment
- * | property
- * | selector
- */
- ident: function() {
- var i = 2
- , la = this.lookahead(i).type;
- while ('space' == la) la = this.lookahead(++i).type;
- switch (la) {
- // Assignment
- case '=':
- case '?=':
- case '-=':
- case '+=':
- case '*=':
- case '/=':
- case '%=':
- return this.assignment();
- // Member
- case '.':
- if ('space' == this.lookahead(i - 1).type) return this.selector();
- if (this._ident == this.peek()) return this.id();
- while ('=' != this.lookahead(++i).type
- && !~['[', ',', 'newline', 'indent', 'eos'].indexOf(this.lookahead(i).type)) ;
- if ('=' == this.lookahead(i).type) {
- this._ident = this.peek();
- return this.expression();
- } else if (this.looksLikeSelector() && this.stateAllowsSelector()) {
- return this.selector();
- }
- // Assignment []=
- case '[':
- if (this._ident == this.peek()) return this.id();
- while (']' != this.lookahead(i++).type
- && 'selector' != this.lookahead(i).type
- && 'eos' != this.lookahead(i).type) ;
- if ('=' == this.lookahead(i).type) {
- this._ident = this.peek();
- return this.expression();
- } else if (this.looksLikeSelector() && this.stateAllowsSelector()) {
- return this.selector();
- }
- // Operation
- case '-':
- case '+':
- case '/':
- case '*':
- case '%':
- case '**':
- case '&&':
- case '||':
- case '>':
- case '<':
- case '>=':
- case '<=':
- case '!=':
- case '==':
- case '?':
- case 'in':
- case 'is a':
- case 'is defined':
- // Prevent cyclic .ident, return literal
- if (this._ident == this.peek()) {
- return this.id();
- } else {
- this._ident = this.peek();
- switch (this.currentState()) {
- // unary op or selector in property / for
- case 'for':
- case 'selector':
- return this.property();
- // Part of a selector
- case 'root':
- case 'atblock':
- case 'atrule':
- return '[' == la
- ? this.subscript()
- : this.selector();
- case 'function':
- case 'conditional':
- return this.looksLikeSelector()
- ? this.selector()
- : this.expression();
- // Do not disrupt the ident when an operand
- default:
- return this.operand
- ? this.id()
- : this.expression();
- }
- }
- // Selector or property
- default:
- switch (this.currentState()) {
- case 'root':
- return this.selector();
- case 'for':
- case 'selector':
- case 'function':
- case 'conditional':
- case 'atblock':
- case 'atrule':
- return this.property();
- default:
- var id = this.id();
- if ('interpolation' == this.previousState()) id.mixin = true;
- return id;
- }
- }
- },
- /**
- * '*'? (ident | '{' expression '}')+
- */
- interpolate: function() {
- var node
- , segs = []
- , star;
- star = this.accept('*');
- if (star) segs.push(new nodes.Literal('*'));
- while (true) {
- if (this.accept('{')) {
- this.state.push('interpolation');
- segs.push(this.expression());
- this.expect('}');
- this.state.pop();
- } else if (node = this.accept('-')){
- segs.push(new nodes.Literal('-'));
- } else if (node = this.accept('ident')){
- segs.push(node.val);
- } else {
- break;
- }
- }
- if (!segs.length) this.expect('ident');
- return segs;
- },
- /**
- * property ':'? expression
- * | ident
- */
- property: function() {
- if (this.looksLikeSelector(true)) return this.selector();
- // property
- var ident = this.interpolate()
- , prop = new nodes.Property(ident)
- , ret = prop;
- // optional ':'
- this.accept('space');
- if (this.accept(':')) this.accept('space');
- this.state.push('property');
- this.inProperty = true;
- prop.expr = this.list();
- if (prop.expr.isEmpty) ret = ident[0];
- this.inProperty = false;
- this.allowPostfix = true;
- this.state.pop();
- // optional ';'
- this.accept(';');
- return ret;
- },
- /**
- * selector ',' selector
- * | selector newline selector
- * | selector block
- */
- selector: function() {
- var arr
- , group = new nodes.Group
- , scope = this.selectorScope
- , isRoot = 'root' == this.currentState()
- , selector;
- do {
- // Clobber newline after ,
- this.accept('newline');
- arr = this.selectorParts();
- // Push the selector
- if (isRoot && scope) arr.unshift(new nodes.Literal(scope + ' '));
- if (arr.length) {
- selector = new nodes.Selector(arr);
- selector.lineno = arr[0].lineno;
- selector.column = arr[0].column;
- group.push(selector);
- }
- } while (this.accept(',') || this.accept('newline'));
- if ('selector-parts' == this.currentState()) return group.nodes;
- this.state.push('selector');
- group.block = this.block(group);
- this.state.pop();
- return group;
- },
- selectorParts: function(){
- var tok
- , arr = [];
- // Selector candidates,
- // stitched together to
- // form a selector.
- while (tok = this.selectorToken()) {
- debug.selector('%s', tok);
- // Selector component
- switch (tok.type) {
- case '{':
- this.skipSpaces();
- var expr = this.expression();
- this.skipSpaces();
- this.expect('}');
- arr.push(expr);
- break;
- case this.prefix && '.':
- var literal = new nodes.Literal(tok.val + this.prefix);
- literal.prefixed = true;
- arr.push(literal);
- break;
- case 'comment':
- // ignore comments
- break;
- case 'color':
- case 'unit':
- arr.push(new nodes.Literal(tok.val.raw));
- break;
- case 'space':
- arr.push(new nodes.Literal(' '));
- break;
- case 'function':
- arr.push(new nodes.Literal(tok.val.name + '('));
- break;
- case 'ident':
- arr.push(new nodes.Literal(tok.val.name || tok.val.string));
- break;
- default:
- arr.push(new nodes.Literal(tok.val));
- if (tok.space) arr.push(new nodes.Literal(' '));
- }
- }
- return arr;
- },
- /**
- * ident ('=' | '?=') expression
- */
- assignment: function() {
- var
- op,
- node,
- ident = this.id(),
- name = ident.name;
- if (op =
- this.accept('=')
- || this.accept('?=')
- || this.accept('+=')
- || this.accept('-=')
- || this.accept('*=')
- || this.accept('/=')
- || this.accept('%=')) {
- this.state.push('assignment');
- var expr = this.list();
- // @block support
- if (expr.isEmpty) this.assignAtblock(expr);
- node = new nodes.Ident(name, expr);
- node.lineno = ident.lineno;
- node.column = ident.column;
- this.state.pop();
- switch (op.type) {
- case '?=':
- var defined = new nodes.BinOp('is defined', node)
- , lookup = new nodes.Expression;
- lookup.push(new nodes.Ident(name));
- node = new nodes.Ternary(defined, lookup, node);
- break;
- case '+=':
- case '-=':
- case '*=':
- case '/=':
- case '%=':
- node.val = new nodes.BinOp(op.type[0], new nodes.Ident(name), expr);
- break;
- }
- }
- return node;
- },
- /**
- * definition
- * | call
- */
- function: function() {
- var parens = 1
- , i = 2
- , tok;
- // Lookahead and determine if we are dealing
- // with a function call or definition. Here
- // we pair parens to prevent false negatives
- out:
- while (tok = this.lookahead(i++)) {
- switch (tok.type) {
- case 'function':
- case '(':
- ++parens;
- break;
- case ')':
- if (!--parens) break out;
- break;
- case 'eos':
- this.error('failed to find closing paren ")"');
- }
- }
- // Definition or call
- switch (this.currentState()) {
- case 'expression':
- return this.functionCall();
- default:
- return this.looksLikeFunctionDefinition(i)
- ? this.functionDefinition()
- : this.expression();
- }
- },
- /**
- * url '(' (expression | urlchars)+ ')'
- */
- url: function() {
- this.expect('function');
- this.state.push('function arguments');
- var args = this.args();
- this.expect(')');
- this.state.pop();
- return new nodes.Call('url', args);
- },
- /**
- * '+'? ident '(' expression ')' block?
- */
- functionCall: function() {
- var withBlock = this.accept('+');
- if ('url' == this.peek().val.name) return this.url();
- var tok = this.expect('function').val;
- var name = tok.name;
- this.state.push('function arguments');
- this.parens++;
- var args = this.args();
- this.expect(')');
- this.parens--;
- this.state.pop();
- var call = new nodes.Call(name, args);
- call.column = tok.column;
- call.lineno = tok.lineno;
- if (withBlock) {
- this.state.push('function');
- call.block = this.block(call);
- this.state.pop();
- }
- return call;
- },
- /**
- * ident '(' params ')' block
- */
- functionDefinition: function() {
- var
- tok = this.expect('function'),
- name = tok.val.name;
- // params
- this.state.push('function params');
- this.skipWhitespace();
- var params = this.params();
- this.skipWhitespace();
- this.expect(')');
- this.state.pop();
- // Body
- this.state.push('function');
- var fn = new nodes.Function(name, params);
- fn.column = tok.column;
- fn.lineno = tok.lineno;
- fn.block = this.block(fn);
- this.state.pop();
- return new nodes.Ident(name, fn);
- },
- /**
- * ident
- * | ident '...'
- * | ident '=' expression
- * | ident ',' ident
- */
- params: function() {
- var tok
- , node
- , params = new nodes.Params;
- while (tok = this.accept('ident')) {
- this.accept('space');
- params.push(node = tok.val);
- if (this.accept('...')) {
- node.rest = true;
- } else if (this.accept('=')) {
- node.val = this.expression();
- }
- this.skipWhitespace();
- this.accept(',');
- this.skipWhitespace();
- }
- return params;
- },
- /**
- * (ident ':')? expression (',' (ident ':')? expression)*
- */
- args: function() {
- var args = new nodes.Arguments
- , keyword;
- do {
- // keyword
- if ('ident' == this.peek().type && ':' == this.lookahead(2).type) {
- keyword = this.next().val.string;
- this.expect(':');
- args.map[keyword] = this.expression();
- // arg
- } else {
- args.push(this.expression());
- }
- } while (this.accept(','));
- return args;
- },
- /**
- * expression (',' expression)*
- */
- list: function() {
- var node = this.expression();
- while (this.accept(',')) {
- if (node.isList) {
- list.push(this.expression());
- } else {
- var list = new nodes.Expression(true);
- list.push(node);
- list.push(this.expression());
- node = list;
- }
- }
- return node;
- },
- /**
- * negation+
- */
- expression: function() {
- var node
- , expr = new nodes.Expression;
- this.state.push('expression');
- while (node = this.negation()) {
- if (!node) this.error('unexpected token {peek} in expression');
- expr.push(node);
- }
- this.state.pop();
- if (expr.nodes.length) {
- expr.lineno = expr.nodes[0].lineno;
- expr.column = expr.nodes[0].column;
- }
- return expr;
- },
- /**
- * 'not' ternary
- * | ternary
- */
- negation: function() {
- if (this.accept('not')) {
- return new nodes.UnaryOp('!', this.negation());
- }
- return this.ternary();
- },
- /**
- * logical ('?' expression ':' expression)?
- */
- ternary: function() {
- var node = this.logical();
- if (this.accept('?')) {
- var trueExpr = this.expression();
- this.expect(':');
- var falseExpr = this.expression();
- node = new nodes.Ternary(node, trueExpr, falseExpr);
- }
- return node;
- },
- /**
- * typecheck (('&&' | '||') typecheck)*
- */
- logical: function() {
- var op
- , node = this.typecheck();
- while (op = this.accept('&&') || this.accept('||')) {
- node = new nodes.BinOp(op.type, node, this.typecheck());
- }
- return node;
- },
- /**
- * equality ('is a' equality)*
- */
- typecheck: function() {
- var op
- , node = this.equality();
- while (op = this.accept('is a')) {
- this.operand = true;
- if (!node) this.error('illegal unary "' + op + '", missing left-hand operand');
- node = new nodes.BinOp(op.type, node, this.equality());
- this.operand = false;
- }
- return node;
- },
- /**
- * in (('==' | '!=') in)*
- */
- equality: function() {
- var op
- , node = this.in();
- while (op = this.accept('==') || this.accept('!=')) {
- this.operand = true;
- if (!node) this.error('illegal unary "' + op + '", missing left-hand operand');
- node = new nodes.BinOp(op.type, node, this.in());
- this.operand = false;
- }
- return node;
- },
- /**
- * relational ('in' relational)*
- */
- in: function() {
- var node = this.relational();
- while (this.accept('in')) {
- this.operand = true;
- if (!node) this.error('illegal unary "in", missing left-hand operand');
- node = new nodes.BinOp('in', node, this.relational());
- this.operand = false;
- }
- return node;
- },
- /**
- * range (('>=' | '<=' | '>' | '<') range)*
- */
- relational: function() {
- var op
- , node = this.range();
- while (op =
- this.accept('>=')
- || this.accept('<=')
- || this.accept('<')
- || this.accept('>')
- ) {
- this.operand = true;
- if (!node) this.error('illegal unary "' + op + '", missing left-hand operand');
- node = new nodes.BinOp(op.type, node, this.range());
- this.operand = false;
- }
- return node;
- },
- /**
- * additive (('..' | '...') additive)*
- */
- range: function() {
- var op
- , node = this.additive();
- if (op = this.accept('...') || this.accept('..')) {
- this.operand = true;
- if (!node) this.error('illegal unary "' + op + '", missing left-hand operand');
- node = new nodes.BinOp(op.val, node, this.additive());
- this.operand = false;
- }
- return node;
- },
- /**
- * multiplicative (('+' | '-') multiplicative)*
- */
- additive: function() {
- var op
- , node = this.multiplicative();
- while (op = this.accept('+') || this.accept('-')) {
- this.operand = true;
- node = new nodes.BinOp(op.type, node, this.multiplicative());
- this.operand = false;
- }
- return node;
- },
- /**
- * defined (('**' | '*' | '/' | '%') defined)*
- */
- multiplicative: function() {
- var op
- , node = this.defined();
- while (op =
- this.accept('**')
- || this.accept('*')
- || this.accept('/')
- || this.accept('%')) {
- this.operand = true;
- if ('/' == op && this.inProperty && !this.parens) {
- this.stash.push(new Token('literal', new nodes.Literal('/')));
- this.operand = false;
- return node;
- } else {
- if (!node) this.error('illegal unary "' + op + '", missing left-hand operand');
- node = new nodes.BinOp(op.type, node, this.defined());
- this.operand = false;
- }
- }
- return node;
- },
- /**
- * unary 'is defined'
- * | unary
- */
- defined: function() {
- var node = this.unary();
- if (this.accept('is defined')) {
- if (!node) this.error('illegal unary "is defined", missing left-hand operand');
- node = new nodes.BinOp('is defined', node);
- }
- return node;
- },
- /**
- * ('!' | '~' | '+' | '-') unary
- * | subscript
- */
- unary: function() {
- var op
- , node;
- if (op =
- this.accept('!')
- || this.accept('~')
- || this.accept('+')
- || this.accept('-')) {
- this.operand = true;
- node = this.unary();
- if (!node) this.error('illegal unary "' + op + '"');
- node = new nodes.UnaryOp(op.type, node);
- this.operand = false;
- return node;
- }
- return this.subscript();
- },
- /**
- * member ('[' expression ']')+ '='?
- * | member
- */
- subscript: function() {
- var node = this.member()
- , id;
- while (this.accept('[')) {
- node = new nodes.BinOp('[]', node, this.expression());
- this.expect(']');
- }
- // TODO: TernaryOp :)
- if (this.accept('=')) {
- node.op += '=';
- node.val = this.list();
- // @block support
- if (node.val.isEmpty) this.assignAtblock(node.val);
- }
- return node;
- },
- /**
- * primary ('.' id)+ '='?
- * | primary
- */
- member: function() {
- var node = this.primary();
- if (node) {
- while (this.accept('.')) {
- var id = new nodes.Ident(this.expect('ident').val.string);
- node = new nodes.Member(node, id);
- }
- this.skipSpaces();
- if (this.accept('=')) {
- node.val = this.list();
- // @block support
- if (node.val.isEmpty) this.assignAtblock(node.val);
- }
- }
- return node;
- },
- /**
- * '{' '}'
- * | '{' pair (ws pair)* '}'
- */
- object: function(){
- var obj = new nodes.Object
- , id, val, comma, hash;
- this.expect('{');
- this.skipWhitespace();
- while (!this.accept('}')) {
- if (this.accept('comment')
- || this.accept('newline')) continue;
- if (!comma) this.accept(',');
- id = this.accept('ident') || this.accept('string');
- if (!id) {
- this.error('expected "ident" or "string", got {peek}');
- }
- hash = id.val.hash;
- this.skipSpacesAndComments();
- this.expect(':');
- val = this.expression();
- obj.setValue(hash, val);
- obj.setKey(hash, id.val);
- comma = this.accept(',');
- this.skipWhitespace();
- }
- return obj;
- },
- /**
- * unit
- * | null
- * | color
- * | string
- * | ident
- * | boolean
- * | literal
- * | object
- * | atblock
- * | atrule
- * | '(' expression ')' '%'?
- */
- primary: function() {
- var tok;
- this.skipSpaces();
- // Parenthesis
- if (this.accept('(')) {
- ++this.parens;
- var expr = this.expression()
- , paren = this.expect(')');
- --this.parens;
- if (this.accept('%')) expr.push(new nodes.Ident('%'));
- tok = this.peek();
- // (1 + 2)px, (1 + 2)em, etc.
- if (!paren.space
- && 'ident' == tok.type
- && ~units.indexOf(tok.val.string)) {
- expr.push(new nodes.Ident(tok.val.string));
- this.next();
- }
- return expr;
- }
- tok = this.peek();
- // Primitive
- switch (tok.type) {
- case 'null':
- case 'unit':
- case 'color':
- case 'string':
- case 'literal':
- case 'boolean':
- case 'comment':
- return this.next().val;
- case !this.cond && '{':
- return this.object();
- case 'atblock':
- return this.atblock();
- // property lookup
- case 'atrule':
- var id = new nodes.Ident(this.next().val);
- id.property = true;
- return id;
- case 'ident':
- return this.ident();
- case 'function':
- return tok.anonymous
- ? this.functionDefinition()
- : this.functionCall();
- }
- }
- };
|