/src/css/Parser.js

https://github.com/tomelam/parser-lib · JavaScript · 1341 lines · 810 code · 241 blank · 290 comment · 118 complexity · f6c7f858fbf68eb1a12ed3c1cc858bc0 MD5 · raw file

  1. /**
  2. * A CSS3 parser.
  3. * @namespace parserlib.css
  4. * @class Parser
  5. * @constructor
  6. * @param {Object} options (Optional) Various options for the parser:
  7. * starHack (true|false) to allow IE6 star hack as valid,
  8. * underscoreHack (true|false) to interpret leading underscores
  9. * as IE6-7 targeting for known properties, ieFilters (true|false)
  10. * to indicate that IE < 8 filters should be accepted and not throw
  11. * syntax errors.
  12. */
  13. function Parser(options){
  14. //inherit event functionality
  15. EventTarget.call(this);
  16. this.options = options || {};
  17. this._tokenStream = null;
  18. }
  19. //Static constants
  20. Parser.DEFAULT_TYPE = 0;
  21. Parser.COMBINATOR_TYPE = 1;
  22. Parser.MEDIA_FEATURE_TYPE = 2;
  23. Parser.MEDIA_QUERY_TYPE = 3;
  24. Parser.PROPERTY_NAME_TYPE = 4;
  25. Parser.PROPERTY_VALUE_TYPE = 5;
  26. Parser.PROPERTY_VALUE_PART_TYPE = 6;
  27. Parser.SELECTOR_TYPE = 7;
  28. Parser.SELECTOR_PART_TYPE = 8;
  29. Parser.SELECTOR_SUB_PART_TYPE = 9;
  30. Parser.prototype = function(){
  31. var proto = new EventTarget(), //new prototype
  32. prop,
  33. additions = {
  34. //restore constructor
  35. constructor: Parser,
  36. //instance constants - yuck
  37. DEFAULT_TYPE : 0,
  38. COMBINATOR_TYPE : 1,
  39. MEDIA_FEATURE_TYPE : 2,
  40. MEDIA_QUERY_TYPE : 3,
  41. PROPERTY_NAME_TYPE : 4,
  42. PROPERTY_VALUE_TYPE : 5,
  43. PROPERTY_VALUE_PART_TYPE : 6,
  44. SELECTOR_TYPE : 7,
  45. SELECTOR_PART_TYPE : 8,
  46. SELECTOR_SUB_PART_TYPE : 9,
  47. //-----------------------------------------------------------------
  48. // Grammar
  49. //-----------------------------------------------------------------
  50. _stylesheet: function(){
  51. /*
  52. * stylesheet
  53. * : [ CHARSET_SYM S* STRING S* ';' ]?
  54. * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]*
  55. * [ namespace [S|CDO|CDC]* ]*
  56. * [ [ ruleset | media | page | font_face | keyframes ] [S|CDO|CDC]* ]*
  57. * ;
  58. */
  59. var tokenStream = this._tokenStream,
  60. charset = null,
  61. token,
  62. tt;
  63. this.fire("startstylesheet");
  64. //try to read character set
  65. this._charset();
  66. this._skipCruft();
  67. //try to read imports - may be more than one
  68. while (tokenStream.peek() == Tokens.IMPORT_SYM){
  69. this._import();
  70. this._skipCruft();
  71. }
  72. //try to read namespaces - may be more than one
  73. while (tokenStream.peek() == Tokens.NAMESPACE_SYM){
  74. this._namespace();
  75. this._skipCruft();
  76. }
  77. //get the next token
  78. tt = tokenStream.peek();
  79. //try to read the rest
  80. while(tt > Tokens.EOF){
  81. try {
  82. switch(tt){
  83. case Tokens.MEDIA_SYM:
  84. this._media();
  85. this._skipCruft();
  86. break;
  87. case Tokens.PAGE_SYM:
  88. this._page();
  89. this._skipCruft();
  90. break;
  91. case Tokens.FONT_FACE_SYM:
  92. this._font_face();
  93. this._skipCruft();
  94. break;
  95. case Tokens.KEYFRAMES_SYM:
  96. this._keyframes();
  97. this._skipCruft();
  98. break;
  99. case Tokens.S:
  100. this._readWhitespace();
  101. break;
  102. default:
  103. if(!this._ruleset()){
  104. //error handling for known issues
  105. switch(tt){
  106. case Tokens.CHARSET_SYM:
  107. token = tokenStream.LT(1);
  108. this._charset(false);
  109. throw new SyntaxError("@charset not allowed here.", token.startLine, token.startCol);
  110. case Tokens.IMPORT_SYM:
  111. token = tokenStream.LT(1);
  112. this._import(false);
  113. throw new SyntaxError("@import not allowed here.", token.startLine, token.startCol);
  114. case Tokens.NAMESPACE_SYM:
  115. token = tokenStream.LT(1);
  116. this._namespace(false);
  117. throw new SyntaxError("@namespace not allowed here.", token.startLine, token.startCol);
  118. default:
  119. tokenStream.get(); //get the last token
  120. this._unexpectedToken(tokenStream.token());
  121. }
  122. }
  123. }
  124. } catch(ex) {
  125. if (ex instanceof SyntaxError && !this.options.strict){
  126. this.fire({
  127. type: "error",
  128. error: ex,
  129. message: ex.message,
  130. line: ex.line,
  131. col: ex.col
  132. });
  133. } else {
  134. throw ex;
  135. }
  136. }
  137. tt = tokenStream.peek();
  138. }
  139. if (tt != Tokens.EOF){
  140. this._unexpectedToken(tokenStream.token());
  141. }
  142. this.fire("endstylesheet");
  143. },
  144. _charset: function(emit){
  145. var tokenStream = this._tokenStream,
  146. charset,
  147. token,
  148. line,
  149. col;
  150. if (tokenStream.match(Tokens.CHARSET_SYM)){
  151. line = tokenStream.token().startLine;
  152. col = tokenStream.token().startCol;
  153. this._readWhitespace();
  154. tokenStream.mustMatch(Tokens.STRING);
  155. token = tokenStream.token();
  156. charset = token.value;
  157. this._readWhitespace();
  158. tokenStream.mustMatch(Tokens.SEMICOLON);
  159. if (emit !== false){
  160. this.fire({
  161. type: "charset",
  162. charset:charset,
  163. line: line,
  164. col: col
  165. });
  166. }
  167. }
  168. },
  169. _import: function(emit){
  170. /*
  171. * import
  172. * : IMPORT_SYM S*
  173. * [STRING|URI] S* media_query_list? ';' S*
  174. */
  175. var tokenStream = this._tokenStream,
  176. tt,
  177. uri,
  178. importToken,
  179. mediaList = [];
  180. //read import symbol
  181. tokenStream.mustMatch(Tokens.IMPORT_SYM);
  182. importToken = tokenStream.token();
  183. this._readWhitespace();
  184. tokenStream.mustMatch([Tokens.STRING, Tokens.URI]);
  185. //grab the URI value
  186. uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1");
  187. this._readWhitespace();
  188. mediaList = this._media_query_list();
  189. //must end with a semicolon
  190. tokenStream.mustMatch(Tokens.SEMICOLON);
  191. this._readWhitespace();
  192. if (emit !== false){
  193. this.fire({
  194. type: "import",
  195. uri: uri,
  196. media: mediaList,
  197. line: importToken.startLine,
  198. col: importToken.startCol
  199. });
  200. }
  201. },
  202. _namespace: function(emit){
  203. /*
  204. * namespace
  205. * : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S*
  206. */
  207. var tokenStream = this._tokenStream,
  208. line,
  209. col,
  210. prefix,
  211. uri;
  212. //read import symbol
  213. tokenStream.mustMatch(Tokens.NAMESPACE_SYM);
  214. line = tokenStream.token().startLine;
  215. col = tokenStream.token().startCol;
  216. this._readWhitespace();
  217. //it's a namespace prefix - no _namespace_prefix() method because it's just an IDENT
  218. if (tokenStream.match(Tokens.IDENT)){
  219. prefix = tokenStream.token().value;
  220. this._readWhitespace();
  221. }
  222. tokenStream.mustMatch([Tokens.STRING, Tokens.URI]);
  223. /*if (!tokenStream.match(Tokens.STRING)){
  224. tokenStream.mustMatch(Tokens.URI);
  225. }*/
  226. //grab the URI value
  227. uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1");
  228. this._readWhitespace();
  229. //must end with a semicolon
  230. tokenStream.mustMatch(Tokens.SEMICOLON);
  231. this._readWhitespace();
  232. if (emit !== false){
  233. this.fire({
  234. type: "namespace",
  235. prefix: prefix,
  236. uri: uri,
  237. line: line,
  238. col: col
  239. });
  240. }
  241. },
  242. _media: function(){
  243. /*
  244. * media
  245. * : MEDIA_SYM S* media_query_list S* '{' S* ruleset* '}' S*
  246. * ;
  247. */
  248. var tokenStream = this._tokenStream,
  249. line,
  250. col,
  251. mediaList;// = [];
  252. //look for @media
  253. tokenStream.mustMatch(Tokens.MEDIA_SYM);
  254. line = tokenStream.token().startLine;
  255. col = tokenStream.token().startCol;
  256. this._readWhitespace();
  257. mediaList = this._media_query_list();
  258. tokenStream.mustMatch(Tokens.LBRACE);
  259. this._readWhitespace();
  260. this.fire({
  261. type: "startmedia",
  262. media: mediaList,
  263. line: line,
  264. col: col
  265. });
  266. while(true) {
  267. if (tokenStream.peek() == Tokens.PAGE_SYM){
  268. this._page();
  269. } else if (!this._ruleset()){
  270. break;
  271. }
  272. }
  273. tokenStream.mustMatch(Tokens.RBRACE);
  274. this._readWhitespace();
  275. this.fire({
  276. type: "endmedia",
  277. media: mediaList,
  278. line: line,
  279. col: col
  280. });
  281. },
  282. //CSS3 Media Queries
  283. _media_query_list: function(){
  284. /*
  285. * media_query_list
  286. * : S* [media_query [ ',' S* media_query ]* ]?
  287. * ;
  288. */
  289. var tokenStream = this._tokenStream,
  290. mediaList = [];
  291. this._readWhitespace();
  292. if (tokenStream.peek() == Tokens.IDENT || tokenStream.peek() == Tokens.LPAREN){
  293. mediaList.push(this._media_query());
  294. }
  295. while(tokenStream.match(Tokens.COMMA)){
  296. this._readWhitespace();
  297. mediaList.push(this._media_query());
  298. }
  299. return mediaList;
  300. },
  301. /*
  302. * Note: "expression" in the grammar maps to the _media_expression
  303. * method.
  304. */
  305. _media_query: function(){
  306. /*
  307. * media_query
  308. * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]*
  309. * | expression [ AND S* expression ]*
  310. * ;
  311. */
  312. var tokenStream = this._tokenStream,
  313. type = null,
  314. ident = null,
  315. token = null,
  316. expressions = [];
  317. if (tokenStream.match(Tokens.IDENT)){
  318. ident = tokenStream.token().value.toLowerCase();
  319. //since there's no custom tokens for these, need to manually check
  320. if (ident != "only" && ident != "not"){
  321. tokenStream.unget();
  322. ident = null;
  323. } else {
  324. token = tokenStream.token();
  325. }
  326. }
  327. this._readWhitespace();
  328. if (tokenStream.peek() == Tokens.IDENT){
  329. type = this._media_type();
  330. if (token === null){
  331. token = tokenStream.token();
  332. }
  333. } else if (tokenStream.peek() == Tokens.LPAREN){
  334. if (token === null){
  335. token = tokenStream.LT(1);
  336. }
  337. expressions.push(this._media_expression());
  338. }
  339. if (type === null && expressions.length === 0){
  340. return null;
  341. } else {
  342. this._readWhitespace();
  343. while (tokenStream.match(Tokens.IDENT)){
  344. if (tokenStream.token().value.toLowerCase() != "and"){
  345. this._unexpectedToken(tokenStream.token());
  346. }
  347. this._readWhitespace();
  348. expressions.push(this._media_expression());
  349. }
  350. }
  351. return new MediaQuery(ident, type, expressions, token.startLine, token.startCol);
  352. },
  353. //CSS3 Media Queries
  354. _media_type: function(){
  355. /*
  356. * media_type
  357. * : IDENT
  358. * ;
  359. */
  360. return this._media_feature();
  361. },
  362. /**
  363. * Note: in CSS3 Media Queries, this is called "expression".
  364. * Renamed here to avoid conflict with CSS3 Selectors
  365. * definition of "expression". Also note that "expr" in the
  366. * grammar now maps to "expression" from CSS3 selectors.
  367. * @method _media_expression
  368. * @private
  369. */
  370. _media_expression: function(){
  371. /*
  372. * expression
  373. * : '(' S* media_feature S* [ ':' S* expr ]? ')' S*
  374. * ;
  375. */
  376. var tokenStream = this._tokenStream,
  377. feature = null,
  378. token,
  379. expression = null;
  380. tokenStream.mustMatch(Tokens.LPAREN);
  381. feature = this._media_feature();
  382. this._readWhitespace();
  383. if (tokenStream.match(Tokens.COLON)){
  384. this._readWhitespace();
  385. token = tokenStream.LT(1);
  386. expression = this._expression();
  387. }
  388. tokenStream.mustMatch(Tokens.RPAREN);
  389. this._readWhitespace();
  390. return new MediaFeature(feature, (expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null));
  391. },
  392. //CSS3 Media Queries
  393. _media_feature: function(){
  394. /*
  395. * media_feature
  396. * : IDENT
  397. * ;
  398. */
  399. var tokenStream = this._tokenStream;
  400. tokenStream.mustMatch(Tokens.IDENT);
  401. return SyntaxUnit.fromToken(tokenStream.token());
  402. },
  403. //CSS3 Paged Media
  404. _page: function(){
  405. /*
  406. * page:
  407. * PAGE_SYM S* IDENT? pseudo_page? S*
  408. * '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S*
  409. * ;
  410. */
  411. var tokenStream = this._tokenStream,
  412. line,
  413. col,
  414. identifier = null,
  415. pseudoPage = null;
  416. //look for @page
  417. tokenStream.mustMatch(Tokens.PAGE_SYM);
  418. line = tokenStream.token().startLine;
  419. col = tokenStream.token().startCol;
  420. this._readWhitespace();
  421. if (tokenStream.match(Tokens.IDENT)){
  422. identifier = tokenStream.token().value;
  423. //The value 'auto' may not be used as a page name and MUST be treated as a syntax error.
  424. if (identifier.toLowerCase() === "auto"){
  425. this._unexpectedToken(tokenStream.token());
  426. }
  427. }
  428. //see if there's a colon upcoming
  429. if (tokenStream.peek() == Tokens.COLON){
  430. pseudoPage = this._pseudo_page();
  431. }
  432. this._readWhitespace();
  433. this.fire({
  434. type: "startpage",
  435. id: identifier,
  436. pseudo: pseudoPage,
  437. line: line,
  438. col: col
  439. });
  440. this._readDeclarations(true, true);
  441. this.fire({
  442. type: "endpage",
  443. id: identifier,
  444. pseudo: pseudoPage,
  445. line: line,
  446. col: col
  447. });
  448. },
  449. //CSS3 Paged Media
  450. _margin: function(){
  451. /*
  452. * margin :
  453. * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S*
  454. * ;
  455. */
  456. var tokenStream = this._tokenStream,
  457. line,
  458. col,
  459. marginSym = this._margin_sym();
  460. if (marginSym){
  461. line = tokenStream.token().startLine;
  462. col = tokenStream.token().startCol;
  463. this.fire({
  464. type: "startpagemargin",
  465. margin: marginSym,
  466. line: line,
  467. col: col
  468. });
  469. this._readDeclarations(true);
  470. this.fire({
  471. type: "endpagemargin",
  472. margin: marginSym,
  473. line: line,
  474. col: col
  475. });
  476. return true;
  477. } else {
  478. return false;
  479. }
  480. },
  481. //CSS3 Paged Media
  482. _margin_sym: function(){
  483. /*
  484. * margin_sym :
  485. * TOPLEFTCORNER_SYM |
  486. * TOPLEFT_SYM |
  487. * TOPCENTER_SYM |
  488. * TOPRIGHT_SYM |
  489. * TOPRIGHTCORNER_SYM |
  490. * BOTTOMLEFTCORNER_SYM |
  491. * BOTTOMLEFT_SYM |
  492. * BOTTOMCENTER_SYM |
  493. * BOTTOMRIGHT_SYM |
  494. * BOTTOMRIGHTCORNER_SYM |
  495. * LEFTTOP_SYM |
  496. * LEFTMIDDLE_SYM |
  497. * LEFTBOTTOM_SYM |
  498. * RIGHTTOP_SYM |
  499. * RIGHTMIDDLE_SYM |
  500. * RIGHTBOTTOM_SYM
  501. * ;
  502. */
  503. var tokenStream = this._tokenStream;
  504. if(tokenStream.match([Tokens.TOPLEFTCORNER_SYM, Tokens.TOPLEFT_SYM,
  505. Tokens.TOPCENTER_SYM, Tokens.TOPRIGHT_SYM, Tokens.TOPRIGHTCORNER_SYM,
  506. Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM,
  507. Tokens.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_SYM,
  508. Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM,
  509. Tokens.LEFTMIDDLE_SYM, Tokens.LEFTBOTTOM_SYM, Tokens.RIGHTTOP_SYM,
  510. Tokens.RIGHTMIDDLE_SYM, Tokens.RIGHTBOTTOM_SYM]))
  511. {
  512. return SyntaxUnit.fromToken(tokenStream.token());
  513. } else {
  514. return null;
  515. }
  516. },
  517. _pseudo_page: function(){
  518. /*
  519. * pseudo_page
  520. * : ':' IDENT
  521. * ;
  522. */
  523. var tokenStream = this._tokenStream;
  524. tokenStream.mustMatch(Tokens.COLON);
  525. tokenStream.mustMatch(Tokens.IDENT);
  526. //TODO: CSS3 Paged Media says only "left", "center", and "right" are allowed
  527. return tokenStream.token().value;
  528. },
  529. _font_face: function(){
  530. /*
  531. * font_face
  532. * : FONT_FACE_SYM S*
  533. * '{' S* declaration [ ';' S* declaration ]* '}' S*
  534. * ;
  535. */
  536. var tokenStream = this._tokenStream,
  537. line,
  538. col;
  539. //look for @page
  540. tokenStream.mustMatch(Tokens.FONT_FACE_SYM);
  541. line = tokenStream.token().startLine;
  542. col = tokenStream.token().startCol;
  543. this._readWhitespace();
  544. this.fire({
  545. type: "startfontface",
  546. line: line,
  547. col: col
  548. });
  549. this._readDeclarations(true);
  550. this.fire({
  551. type: "endfontface",
  552. line: line,
  553. col: col
  554. });
  555. },
  556. _operator: function(){
  557. /*
  558. * operator
  559. * : '/' S* | ',' S* | /( empty )/
  560. * ;
  561. */
  562. var tokenStream = this._tokenStream,
  563. token = null;
  564. if (tokenStream.match([Tokens.SLASH, Tokens.COMMA])){
  565. token = tokenStream.token();
  566. this._readWhitespace();
  567. }
  568. return token ? PropertyValuePart.fromToken(token) : null;
  569. },
  570. _combinator: function(){
  571. /*
  572. * combinator
  573. * : PLUS S* | GREATER S* | TILDE S* | S+
  574. * ;
  575. */
  576. var tokenStream = this._tokenStream,
  577. value = null,
  578. token;
  579. if(tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])){
  580. token = tokenStream.token();
  581. value = new Combinator(token.value, token.startLine, token.startCol);
  582. this._readWhitespace();
  583. }
  584. return value;
  585. },
  586. _unary_operator: function(){
  587. /*
  588. * unary_operator
  589. * : '-' | '+'
  590. * ;
  591. */
  592. var tokenStream = this._tokenStream;
  593. if (tokenStream.match([Tokens.MINUS, Tokens.PLUS])){
  594. return tokenStream.token().value;
  595. } else {
  596. return null;
  597. }
  598. },
  599. _property: function(){
  600. /*
  601. * property
  602. * : IDENT S*
  603. * ;
  604. */
  605. var tokenStream = this._tokenStream,
  606. value = null,
  607. hack = null,
  608. tokenValue,
  609. token,
  610. line,
  611. col;
  612. //check for star hack - throws error if not allowed
  613. if (tokenStream.peek() == Tokens.STAR && this.options.starHack){
  614. tokenStream.get();
  615. token = tokenStream.token();
  616. hack = token.value;
  617. line = token.startLine;
  618. col = token.startCol;
  619. }
  620. if(tokenStream.match(Tokens.IDENT)){
  621. token = tokenStream.token();
  622. tokenValue = token.value;
  623. //check for underscore hack - no error if not allowed because it's valid CSS syntax
  624. if (tokenValue.charAt(0) == "_" && this.options.underscoreHack){
  625. hack = "_";
  626. tokenValue = tokenValue.substring(1);
  627. }
  628. value = new PropertyName(tokenValue, hack, (line||token.startLine), (col||token.startCol));
  629. this._readWhitespace();
  630. }
  631. return value;
  632. },
  633. //Augmented with CSS3 Selectors
  634. _ruleset: function(){
  635. /*
  636. * ruleset
  637. * : selectors_group
  638. * '{' S* declaration? [ ';' S* declaration? ]* '}' S*
  639. * ;
  640. */
  641. var tokenStream = this._tokenStream,
  642. tt,
  643. selectors;
  644. /*
  645. * Error Recovery: If even a single selector fails to parse,
  646. * then the entire ruleset should be thrown away.
  647. */
  648. try {
  649. selectors = this._selectors_group();
  650. } catch (ex){
  651. if (ex instanceof SyntaxError && !this.options.strict){
  652. //fire error event
  653. this.fire({
  654. type: "error",
  655. error: ex,
  656. message: ex.message,
  657. line: ex.line,
  658. col: ex.col
  659. });
  660. //skip over everything until closing brace
  661. tt = tokenStream.advance([Tokens.RBRACE]);
  662. if (tt == Tokens.RBRACE){
  663. //if there's a right brace, the rule is finished so don't do anything
  664. } else {
  665. //otherwise, rethrow the error because it wasn't handled properly
  666. throw ex;
  667. }
  668. } else {
  669. //not a syntax error, rethrow it
  670. throw ex;
  671. }
  672. //trigger parser to continue
  673. return true;
  674. }
  675. //if it got here, all selectors parsed
  676. if (selectors){
  677. this.fire({
  678. type: "startrule",
  679. selectors: selectors,
  680. line: selectors[0].line,
  681. col: selectors[0].col
  682. });
  683. this._readDeclarations(true);
  684. this.fire({
  685. type: "endrule",
  686. selectors: selectors,
  687. line: selectors[0].line,
  688. col: selectors[0].col
  689. });
  690. }
  691. return selectors;
  692. },
  693. //CSS3 Selectors
  694. _selectors_group: function(){
  695. /*
  696. * selectors_group
  697. * : selector [ COMMA S* selector ]*
  698. * ;
  699. */
  700. var tokenStream = this._tokenStream,
  701. selectors = [],
  702. selector;
  703. selector = this._selector();
  704. if (selector !== null){
  705. selectors.push(selector);
  706. while(tokenStream.match(Tokens.COMMA)){
  707. this._readWhitespace();
  708. selector = this._selector();
  709. if (selector !== null){
  710. selectors.push(selector);
  711. } else {
  712. this._unexpectedToken(tokenStream.LT(1));
  713. }
  714. }
  715. }
  716. return selectors.length ? selectors : null;
  717. },
  718. //CSS3 Selectors
  719. _selector: function(){
  720. /*
  721. * selector
  722. * : simple_selector_sequence [ combinator simple_selector_sequence ]*
  723. * ;
  724. */
  725. var tokenStream = this._tokenStream,
  726. selector = [],
  727. nextSelector = null,
  728. combinator = null,
  729. ws = null;
  730. //if there's no simple selector, then there's no selector
  731. nextSelector = this._simple_selector_sequence();
  732. if (nextSelector === null){
  733. return null;
  734. }
  735. selector.push(nextSelector);
  736. do {
  737. //look for a combinator
  738. combinator = this._combinator();
  739. if (combinator !== null){
  740. selector.push(combinator);
  741. nextSelector = this._simple_selector_sequence();
  742. //there must be a next selector
  743. if (nextSelector === null){
  744. this._unexpectedToken(this.LT(1));
  745. } else {
  746. //nextSelector is an instance of SelectorPart
  747. selector.push(nextSelector);
  748. }
  749. } else {
  750. //if there's not whitespace, we're done
  751. if (this._readWhitespace()){
  752. //add whitespace separator
  753. ws = new Combinator(tokenStream.token().value, tokenStream.token().startLine, tokenStream.token().startCol);
  754. //combinator is not required
  755. combinator = this._combinator();
  756. //selector is required if there's a combinator
  757. nextSelector = this._simple_selector_sequence();
  758. if (nextSelector === null){
  759. if (combinator !== null){
  760. this._unexpectedToken(tokenStream.LT(1));
  761. }
  762. } else {
  763. if (combinator !== null){
  764. selector.push(combinator);
  765. } else {
  766. selector.push(ws);
  767. }
  768. selector.push(nextSelector);
  769. }
  770. } else {
  771. break;
  772. }
  773. }
  774. } while(true);
  775. return new Selector(selector, selector[0].line, selector[0].col);
  776. },
  777. //CSS3 Selectors
  778. _simple_selector_sequence: function(){
  779. /*
  780. * simple_selector_sequence
  781. * : [ type_selector | universal ]
  782. * [ HASH | class | attrib | pseudo | negation ]*
  783. * | [ HASH | class | attrib | pseudo | negation ]+
  784. * ;
  785. */
  786. var tokenStream = this._tokenStream,
  787. //parts of a simple selector
  788. elementName = null,
  789. modifiers = [],
  790. //complete selector text
  791. selectorText= "",
  792. //the different parts after the element name to search for
  793. components = [
  794. //HASH
  795. function(){
  796. return tokenStream.match(Tokens.HASH) ?
  797. new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) :
  798. null;
  799. },
  800. this._class,
  801. this._attrib,
  802. this._pseudo,
  803. this._negation
  804. ],
  805. i = 0,
  806. len = components.length,
  807. component = null,
  808. found = false,
  809. line,
  810. col;
  811. //get starting line and column for the selector
  812. line = tokenStream.LT(1).startLine;
  813. col = tokenStream.LT(1).startCol;
  814. elementName = this._type_selector();
  815. if (!elementName){
  816. elementName = this._universal();
  817. }
  818. if (elementName !== null){
  819. selectorText += elementName;
  820. }
  821. while(true){
  822. //whitespace means we're done
  823. if (tokenStream.peek() === Tokens.S){
  824. break;
  825. }
  826. //check for each component
  827. while(i < len && component === null){
  828. component = components[i++].call(this);
  829. }
  830. if (component === null){
  831. //we don't have a selector
  832. if (selectorText === ""){
  833. return null;
  834. } else {
  835. break;
  836. }
  837. } else {
  838. i = 0;
  839. modifiers.push(component);
  840. selectorText += component.toString();
  841. component = null;
  842. }
  843. }
  844. return selectorText !== "" ?
  845. new SelectorPart(elementName, modifiers, selectorText, line, col) :
  846. null;
  847. },
  848. //CSS3 Selectors
  849. _type_selector: function(){
  850. /*
  851. * type_selector
  852. * : [ namespace_prefix ]? element_name
  853. * ;
  854. */
  855. var tokenStream = this._tokenStream,
  856. ns = this._namespace_prefix(),
  857. elementName = this._element_name();
  858. if (!elementName){
  859. /*
  860. * Need to back out the namespace that was read due to both
  861. * type_selector and universal reading namespace_prefix
  862. * first. Kind of hacky, but only way I can figure out
  863. * right now how to not change the grammar.
  864. */
  865. if (ns){
  866. tokenStream.unget();
  867. if (ns.length > 1){
  868. tokenStream.unget();
  869. }
  870. }
  871. return null;
  872. } else {
  873. if (ns){
  874. elementName.text = ns + elementName.text;
  875. elementName.col -= ns.length;
  876. }
  877. return elementName;
  878. }
  879. },
  880. //CSS3 Selectors
  881. _class: function(){
  882. /*
  883. * class
  884. * : '.' IDENT
  885. * ;
  886. */
  887. var tokenStream = this._tokenStream,
  888. token;
  889. if (tokenStream.match(Tokens.DOT)){
  890. tokenStream.mustMatch(Tokens.IDENT);
  891. token = tokenStream.token();
  892. return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1);
  893. } else {
  894. return null;
  895. }
  896. },
  897. //CSS3 Selectors
  898. _element_name: function(){
  899. /*
  900. * element_name
  901. * : IDENT
  902. * ;
  903. */
  904. var tokenStream = this._tokenStream,
  905. token;
  906. if (tokenStream.match(Tokens.IDENT)){
  907. token = tokenStream.token();
  908. return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol);
  909. } else {
  910. return null;
  911. }
  912. },
  913. //CSS3 Selectors
  914. _namespace_prefix: function(){
  915. /*
  916. * namespace_prefix
  917. * : [ IDENT | '*' ]? '|'
  918. * ;
  919. */
  920. var tokenStream = this._tokenStream,
  921. value = "";
  922. //verify that this is a namespace prefix
  923. if (tokenStream.LA(1) === Tokens.PIPE || tokenStream.LA(2) === Tokens.PIPE){
  924. if(tokenStream.match([Tokens.IDENT, Tokens.STAR])){
  925. value += tokenStream.token().value;
  926. }
  927. tokenStream.mustMatch(Tokens.PIPE);
  928. value += "|";
  929. }
  930. return value.length ? value : null;
  931. },
  932. //CSS3 Selectors
  933. _universal: function(){
  934. /*
  935. * universal
  936. * : [ namespace_prefix ]? '*'
  937. * ;
  938. */
  939. var tokenStream = this._tokenStream,
  940. value = "",
  941. ns;
  942. ns = this._namespace_prefix();
  943. if(ns){
  944. value += ns;
  945. }
  946. if(tokenStream.match(Tokens.STAR)){
  947. value += "*";
  948. }
  949. return value.length ? value : null;
  950. },
  951. //CSS3 Selectors
  952. _attrib: function(){
  953. /*
  954. * attrib
  955. * : '[' S* [ namespace_prefix ]? IDENT S*
  956. * [ [ PREFIXMATCH |
  957. * SUFFIXMATCH |
  958. * SUBSTRINGMATCH |
  959. * '=' |
  960. * INCLUDES |
  961. * DASHMATCH ] S* [ IDENT | STRING ] S*
  962. * ]? ']'
  963. * ;
  964. */
  965. var tokenStream = this._tokenStream,
  966. value = null,
  967. ns,
  968. token;
  969. if (tokenStream.match(Tokens.LBRACKET)){
  970. token = tokenStream.token();
  971. value = token.value;
  972. value += this._readWhitespace();
  973. ns = this._namespace_prefix();
  974. if (ns){
  975. value += ns;
  976. }
  977. tokenStream.mustMatch(Tokens.IDENT);
  978. value += tokenStream.token().value;
  979. value += this._readWhitespace();
  980. if(tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH,
  981. Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])){
  982. value += tokenStream.token().value;
  983. value += this._readWhitespace();
  984. tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]);
  985. value += tokenStream.token().value;
  986. value += this._readWhitespace();
  987. }
  988. tokenStream.mustMatch(Tokens.RBRACKET);
  989. return new SelectorSubPart(value + "]", "attribute", token.startLine, token.startCol);
  990. } else {
  991. return null;
  992. }
  993. },
  994. //CSS3 Selectors
  995. _pseudo: function(){
  996. /*
  997. * pseudo
  998. * : ':' ':'? [ IDENT | functional_pseudo ]
  999. * ;
  1000. */
  1001. var tokenStream = this._tokenStream,
  1002. pseudo = null,
  1003. colons = ":",
  1004. line,
  1005. col;
  1006. if (tokenStream.match(Tokens.COLON)){
  1007. if (tokenStream.match(Tokens.COLON)){
  1008. colons += ":";
  1009. }
  1010. if (tokenStream.match(Tokens.IDENT)){
  1011. pseudo = tokenStream.token().value;
  1012. line = tokenStream.token().startLine;
  1013. col = tokenStream.token().startCol - colons.length;
  1014. } else if (tokenStream.peek() == Tokens.FUNCTION){
  1015. line = tokenStream.LT(1).startLine;
  1016. col = tokenStream.LT(1).startCol - colons.length;
  1017. pseudo = this._functional_pseudo();
  1018. }
  1019. if (pseudo){
  1020. pseudo = new SelectorSubPart(colons + pseudo, "pseudo", line, col);
  1021. }
  1022. }
  1023. return pseudo;
  1024. },
  1025. //CSS3 Selectors
  1026. _functional_pseudo: function(){
  1027. /*
  1028. * functional_pseudo
  1029. * : FUNCTION S* expression ')'
  1030. * ;
  1031. */
  1032. var tokenStream = this._tokenStream,
  1033. value = null;
  1034. if(tokenStream.match(Tokens.FUNCTION)){
  1035. value = tokenStream.token().value;
  1036. value += this._readWhitespace();
  1037. value += this._expression();
  1038. tokenStream.mustMatch(Tokens.RPAREN);
  1039. value += ")";
  1040. }
  1041. return value;
  1042. },
  1043. //CSS3 Selectors
  1044. _expression: function(){
  1045. /*
  1046. * expression
  1047. * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+
  1048. * ;
  1049. */
  1050. var tokenStream = this._tokenStream,
  1051. value = "";
  1052. while(tokenStream.match([Tokens.PLUS, Tokens.MINUS, Tokens.DIMENSION,
  1053. Tokens.NUMBER, Tokens.STRING, Tokens.IDENT, Tokens.LENGTH,
  1054. Tokens.FREQ, Tokens.ANGLE, Tokens.TIME,
  1055. Tokens.RESOLUTION])){
  1056. value += tokenStream.token().value;
  1057. value += this._readWhitespace();
  1058. }
  1059. return value.length ? value : null;
  1060. },
  1061. //CSS3 Selectors
  1062. _negation: function(){
  1063. /*
  1064. * negation
  1065. * : NOT S* negation_arg S* ')'
  1066. * ;
  1067. */
  1068. var tokenStream = this._tokenStream,
  1069. line,
  1070. col,
  1071. value = "",
  1072. arg,
  1073. subpart = null;
  1074. if (tokenStream.match(Tokens.NOT)){
  1075. value = tokenStream.token().value;
  1076. line = tokenStream.token().startLine;
  1077. col = tokenStream.token().startCol;
  1078. value += this._readWhitespace();
  1079. arg = this._negation_arg();
  1080. value += arg;
  1081. value += this._readWhitespace();
  1082. tokenStream.match(Tokens.RPAREN);
  1083. value += tokenStream.token().value;
  1084. subpart = new SelectorSubPart(value, "not", line, col);
  1085. subpart.args.push(arg);
  1086. }
  1087. return subpart;
  1088. },