The parser for SCSS. It parses a string of code into a tree of {Sass::Tree::Node}s.
@param str [String, StringScanner] The source document to parse.
Note that `Parser` *won't* raise a nice error message if this isn't properly parsed; for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
@param line [Fixnum] The line on which the source string appeared,
if it's part of another document
# File lib/sass/scss/parser.rb, line 14 14: def initialize(str, line = 1) 15: @template = str 16: @line = line 17: @strs = [] 18: end
@private
# File lib/sass/scss/parser.rb, line 785 785: def self.expected(scanner, expected, line) 786: pos = scanner.pos 787: 788: after = scanner.string[0...pos] 789: # Get rid of whitespace between pos and the last token, 790: # but only if there's a newline in there 791: after.gsub!(/\s*\n\s*$/, '') 792: # Also get rid of stuff before the last newline 793: after.gsub!(/.*\n/, '') 794: after = "..." + after[15..1] if after.size > 18 795: 796: was = scanner.rest.dup 797: # Get rid of whitespace between pos and the next token, 798: # but only if there's a newline in there 799: was.gsub!(/^\s*\n\s*/, '') 800: # Also get rid of stuff after the next newline 801: was.gsub!(/\n.*/, '') 802: was = was[0...15] + "..." if was.size > 18 803: 804: raise Sass::SyntaxError.new( 805: "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"", 806: :line => line) 807: end
Parses an SCSS document.
@return [Sass::Tree::RootNode] The root node of the document tree @raise [Sass::SyntaxError] if there’s a syntax error in the document
# File lib/sass/scss/parser.rb, line 24 24: def parse 25: init_scanner! 26: root = stylesheet 27: expected("selector or at-rule") unless @scanner.eos? 28: root 29: end
Parses an identifier with interpolation. Note that this won’t assert that the identifier takes up the entire input string; it’s meant to be used with `StringScanner`s as part of other parsers.
@return [Array
The interpolated identifier, or nil if none could be parsed
# File lib/sass/scss/parser.rb, line 37 37: def parse_interp_ident 38: init_scanner! 39: interp_ident 40: end
# File lib/sass/scss/parser.rb, line 679 679: def _interp_string(type) 680: return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]]) 681: res = [start] 682: 683: mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, true]] 684: # @scanner[2].empty? means we've started an interpolated section 685: while @scanner[2] == '#{' 686: @scanner.pos -= 2 # Don't consume the #{ 687: res.last.slice!(2..1) 688: res << expr!(:interpolation) << tok(mid_re) 689: end 690: res 691: end
# File lib/sass/scss/parser.rb, line 409 409: def _selector 410: # The combinator here allows the "> E" hack 411: return unless val = combinator || simple_selector_sequence 412: nl = str{ss}.include?("\n") 413: res = [] 414: res << val 415: res << "\n" if nl 416: 417: while val = combinator || simple_selector_sequence 418: res << val 419: res << "\n" if str{ss}.include?("\n") 420: end 421: Selector::Sequence.new(res.compact) 422: end
# File lib/sass/scss/parser.rb, line 493 493: def attrib 494: return unless tok(/\[/) 495: ss 496: ns, name = attrib_name! 497: ss 498: 499: if op = tok(/=/) || 500: tok(INCLUDES) || 501: tok(DASHMATCH) || 502: tok(PREFIXMATCH) || 503: tok(SUFFIXMATCH) || 504: tok(SUBSTRINGMATCH) 505: @expected = "identifier or string" 506: ss 507: if val = tok(IDENT) 508: val = [val] 509: else 510: val = expr!(:interp_string) 511: end 512: ss 513: end 514: tok(/\]/) 515: 516: Selector::Attribute.new(merge(name), merge(ns), op, merge(val)) 517: end
# File lib/sass/scss/parser.rb, line 519 519: def attrib_name! 520: if name_or_ns = interp_ident 521: # E, E|E 522: if tok(/\|(?!=)/) 523: ns = name_or_ns 524: name = interp_ident 525: else 526: name = name_or_ns 527: end 528: else 529: # *|E or |E 530: ns = [tok(/\*/) || ""] 531: tok!(/\|/) 532: name = expr!(:interp_ident) 533: end 534: return ns, name 535: end
# File lib/sass/scss/parser.rb, line 310 310: def block(node, context) 311: node.has_children = true 312: tok!(/\{/) 313: block_contents(node, context) 314: tok!(/\}/) 315: node 316: end
# File lib/sass/scss/parser.rb, line 329 329: def block_child(context) 330: return variable || directive || ruleset if context == :stylesheet 331: variable || directive || declaration_or_ruleset 332: end
A block may contain declarations and/or rulesets
# File lib/sass/scss/parser.rb, line 319 319: def block_contents(node, context) 320: block_given? ? yield : ss_comments(node) 321: node << (child = block_child(context)) 322: while tok(/;/) || (child && child.has_children) 323: block_given? ? yield : ss_comments(node) 324: node << (child = block_child(context)) 325: end 326: node 327: end
# File lib/sass/scss/parser.rb, line 461 461: def class_selector 462: return unless tok(/\./) 463: @expected = "class name" 464: Selector::Class.new(merge(expr!(:interp_ident))) 465: end
# File lib/sass/scss/parser.rb, line 424 424: def combinator 425: tok(PLUS) || tok(GREATER) || tok(TILDE) 426: end
# File lib/sass/scss/parser.rb, line 147 147: def debug_directive 148: node(Sass::Tree::DebugNode.new(sass_script(:parse))) 149: end
# File lib/sass/scss/parser.rb, line 569 569: def declaration 570: # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks 571: if s = tok(/[:\*\.]|\#(?!\{)/) 572: @use_property_exception = s !~ /[\.\#]/ 573: name = [s, str{ss}, *expr!(:interp_ident)] 574: else 575: return unless name = interp_ident 576: name = [name] if name.is_a?(String) 577: end 578: if comment = tok(COMMENT) 579: name << comment 580: end 581: ss 582: 583: tok!(/:/) 584: space, value = value! 585: ss 586: require_block = tok?(/\{/) 587: 588: node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new)) 589: 590: return node unless require_block 591: nested_properties! node, space 592: end
This is a nasty hack, and the only place in the parser that requires backtracking. The reason is that we can’t figure out if certain strings are declarations or rulesets with fixed finite lookahead. For example, “foo:bar baz baz baz...“ could be either a property or a selector.
To handle this, we simply check if it works as a property (which is the most common case) and, if it doesn’t, try it as a ruleset.
We could eke some more efficiency out of this by handling some easy cases (first token isn’t an identifier, no colon after the identifier, whitespace after the colon), but I’m not sure the gains would be worth the added complexity.
# File lib/sass/scss/parser.rb, line 349 349: def declaration_or_ruleset 350: pos = @scanner.pos 351: line = @line 352: old_use_property_exception, @use_property_exception = 353: @use_property_exception, false 354: begin 355: decl = declaration 356: unless decl && decl.has_children 357: # We want an exception if it's not there, 358: # but we don't want to consume if it is 359: tok!(/[;}]/) unless tok?(/[;}]/) 360: end 361: return decl 362: rescue Sass::SyntaxError => decl_err 363: end 364: 365: @line = line 366: @scanner.pos = pos 367: 368: begin 369: return ruleset 370: rescue Sass::SyntaxError => ruleset_err 371: raise @use_property_exception ? decl_err : ruleset_err 372: end 373: ensure 374: @use_property_exception = old_use_property_exception 375: end
# File lib/sass/scss/parser.rb, line 103 103: def directive 104: return unless tok(/@/) 105: name = tok!(IDENT) 106: ss 107: 108: if dir = special_directive(name) 109: return dir 110: end 111: 112: val = str do 113: # Most at-rules take expressions (e.g. @import), 114: # but some (e.g. @page) take selector-like arguments 115: expr || selector 116: end 117: node = node(Sass::Tree::DirectiveNode.new("@#{name} #{val}".strip)) 118: 119: if tok(/\{/) 120: node.has_children = true 121: block_contents(node, :directive) 122: tok!(/\}/) 123: end 124: 125: node 126: end
# File lib/sass/scss/parser.rb, line 473 473: def element_name 474: return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "") 475: if tok(/\|/) 476: @expected = "element name or *" 477: ns = name 478: name = interp_ident || tok!(/\*/) 479: end 480: 481: if name == '*' 482: Selector::Universal.new(merge(ns)) 483: else 484: Selector::Element.new(merge(name), merge(ns)) 485: end 486: end
# File lib/sass/scss/parser.rb, line 193 193: def else_block(node) 194: return unless tok(/@else/) 195: ss 196: else_node = block( 197: Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), 198: :directive) 199: node.add_else(else_node) 200: pos = @scanner.pos 201: ss 202: 203: else_block(node) || 204: begin 205: # Backtrack in case there are any comments we want to parse 206: @scanner.pos = pos 207: node 208: end 209: end
# File lib/sass/scss/parser.rb, line 780 780: def expected(name) 781: self.class.expected(@scanner, @expected || name, @line) 782: end
# File lib/sass/scss/parser.rb, line 632 632: def expr 633: return unless t = term 634: res = [t, str{ss}] 635: 636: while (o = operator) && (t = term) 637: res << o << t << str{ss} 638: end 639: 640: res 641: end
# File lib/sass/scss/parser.rb, line 762 762: def expr!(name) 763: (e = send(name)) && (return e) 764: expected(EXPR_NAMES[name] || name.to_s) 765: end
# File lib/sass/scss/parser.rb, line 211 211: def extend_directive 212: node(Sass::Tree::ExtendNode.new(expr!(:selector))) 213: end
# File lib/sass/scss/parser.rb, line 155 155: def for_directive 156: tok!(/\$/) 157: var = tok! IDENT 158: ss 159: 160: tok!(/from/) 161: from = sass_script(:parse_until, Set["to", "through"]) 162: ss 163: 164: @expected = '"to" or "through"' 165: exclusive = (tok(/to/) || tok!(/through/)) == 'to' 166: to = sass_script(:parse) 167: ss 168: 169: block(node(Sass::Tree::ForNode.new(var, from, to, exclusive)), :directive) 170: end
# File lib/sass/scss/parser.rb, line 660 660: def function 661: return unless name = tok(FUNCTION) 662: if name == "expression(" || name == "calc(" 663: str, _ = Haml::Shared.balance(@scanner, ((, )), 1) 664: [name, str] 665: else 666: [name, str{ss}, expr, tok!(/\)/)] 667: end 668: end
# File lib/sass/scss/parser.rb, line 467 467: def id_selector 468: return unless tok(/#(?!\{)/) 469: @expected = "id name" 470: Selector::Id.new(merge(expr!(:interp_name))) 471: end
# File lib/sass/scss/parser.rb, line 178 178: def if_directive 179: expr = sass_script(:parse) 180: ss 181: node = block(node(Sass::Tree::IfNode.new(expr)), :directive) 182: pos = @scanner.pos 183: ss 184: 185: else_block(node) || 186: begin 187: # Backtrack in case there are any comments we want to parse 188: @scanner.pos = pos 189: node 190: end 191: end
# File lib/sass/scss/parser.rb, line 215 215: def import_directive 216: @expected = "string or url()" 217: arg = tok(STRING) || (uri = tok!(URI)) 218: path = @scanner[1] || @scanner[2] || @scanner[3] 219: ss 220: 221: media = str {media_query_list}.strip 222: 223: if uri || path =~ /^http:\/\// || !media.strip.empty? || use_css_import? 224: return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip)) 225: end 226: 227: node(Sass::Tree::ImportNode.new(path.strip)) 228: end
# File lib/sass/scss/parser.rb, line 140 140: def include_directive 141: name = tok! IDENT 142: args = sass_script(:parse_mixin_include_arglist) 143: ss 144: node(Sass::Tree::MixinNode.new(name, args)) 145: end
# File lib/sass/scss/parser.rb, line 46 46: def init_scanner! 47: @scanner = 48: if @template.is_a?(StringScanner) 49: @template 50: else 51: StringScanner.new(@template.gsub("\r", "")) 52: end 53: end
# File lib/sass/scss/parser.rb, line 693 693: def interp_ident(start = IDENT) 694: return unless val = tok(start) || interpolation 695: res = [val] 696: while val = tok(NAME) || interpolation 697: res << val 698: end 699: res 700: end
# File lib/sass/scss/parser.rb, line 702 702: def interp_name 703: interp_ident NAME 704: end
# File lib/sass/scss/parser.rb, line 675 675: def interp_string 676: _interp_string(:double) || _interp_string(:single) 677: end
# File lib/sass/scss/parser.rb, line 670 670: def interpolation 671: return unless tok(INTERP_START) 672: sass_script(:parse_interpolated) 673: end
# File lib/sass/scss/parser.rb, line 488 488: def interpolation_selector 489: return unless script = interpolation 490: Selector::Interpolation.new(script) 491: end
# File lib/sass/scss/parser.rb, line 232 232: def media_directive 233: val = str {media_query_list}.strip 234: block(node(Sass::Tree::DirectiveNode.new("@media #{val}")), :directive) 235: end
# File lib/sass/scss/parser.rb, line 267 267: def media_expr 268: return unless tok(/\(/) 269: ss 270: @expected = "media feature (e.g. min-device-width, color)" 271: tok!(IDENT) 272: ss 273: 274: if tok(/:/) 275: ss; expr!(:expr) 276: end 277: tok!(/\)/) 278: ss 279: 280: true 281: end
# File lib/sass/scss/parser.rb, line 249 249: def media_query 250: if tok(/only|not/) 251: ss 252: @expected = "media type (e.g. print, screen)" 253: tok!(IDENT) 254: ss 255: elsif !tok(IDENT) && !media_expr 256: return 257: end 258: 259: ss 260: while tok(/and/) 261: ss; expr!(:media_expr); ss 262: end 263: 264: true 265: end
www.w3.org/TR/css3-mediaqueries/#syntax
# File lib/sass/scss/parser.rb, line 238 238: def media_query_list 239: return unless media_query 240: 241: ss 242: while tok(/,/) 243: ss; expr!(:media_query); ss 244: end 245: 246: true 247: end
# File lib/sass/scss/parser.rb, line 739 739: def merge(arr) 740: arr && Haml::Util.merge_adjacent_strings([arr].flatten) 741: end
# File lib/sass/scss/parser.rb, line 133 133: def mixin_directive 134: name = tok! IDENT 135: args = sass_script(:parse_mixin_definition_arglist) 136: ss 137: block(node(Sass::Tree::MixinDefNode.new(name, args)), :directive) 138: end
# File lib/sass/scss/parser.rb, line 560 560: def negation 561: return unless name = tok(NOT) || tok(MOZ_ANY) 562: ss 563: @expected = "selector" 564: sel = selector_comma_sequence 565: tok!(/\)/) 566: Selector::SelectorPseudoClass.new(name[1...1], sel) 567: end
# File lib/sass/scss/parser.rb, line 621 621: def nested_properties!(node, space) 622: raise Sass::SyntaxError.new(Invalid CSS: a space is required between a property and its definitionwhen it has other properties nested beneath it., :line => @line) unless space 623: 624: @use_property_exception = true 625: @expected = 'expression (e.g. 1px, bold) or "{"' 626: block(node, :property) 627: end
# File lib/sass/scss/parser.rb, line 721 721: def node(node) 722: node.line = @line 723: node 724: end
# File lib/sass/scss/parser.rb, line 293 293: def operator 294: # Many of these operators (all except / and ,) 295: # are disallowed by the CSS spec, 296: # but they're included here for compatibility 297: # with some proprietary MS properties 298: str {ss if tok(/[\/,:.=]/)} 299: end
# File lib/sass/scss/parser.rb, line 456 456: def parent_selector 457: return unless tok(/&/) 458: Selector::Parent.new 459: end
# File lib/sass/scss/parser.rb, line 610 610: def plain_value 611: return unless tok(/:/) 612: space = !str {ss}.empty? 613: @use_property_exception ||= space || !tok?(IDENT) 614: 615: expression = expr 616: expression << tok(IMPORTANT) if expression 617: # expression, space, value 618: return expression, space, expression || [""] 619: end
# File lib/sass/scss/parser.rb, line 89 89: def process_comment(text, node) 90: single_line = text =~ /^\/\// 91: pre_str = single_line ? "" : @scanner. 92: string[0...@scanner.pos]. 93: reverse[/.*?\*\/(.*?)($|\Z)/, 1]. 94: reverse.gsub(/[^\s]/, ' ') 95: text = text.sub(/^\s*\/\//, '/*').gsub(/^\s*\/\//, ' *') + ' */' if single_line 96: comment = Sass::Tree::CommentNode.new(pre_str + text, single_line) 97: comment.line = @line - text.count("\n") 98: node << comment 99: end
# File lib/sass/scss/parser.rb, line 537 537: def pseudo 538: return unless s = tok(/::?/) 539: @expected = "pseudoclass or pseudoelement" 540: name = expr!(:interp_ident) 541: if tok(/\(/) 542: ss 543: arg = expr!(:pseudo_expr) 544: tok!(/\)/) 545: end 546: Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg)) 547: end
# File lib/sass/scss/parser.rb, line 549 549: def pseudo_expr 550: return unless e = tok(PLUS) || tok(/-/) || tok(NUMBER) || 551: interp_string || tok(IDENT) || interpolation 552: res = [e, str{ss}] 553: while e = tok(PLUS) || tok(/-/) || tok(NUMBER) || 554: interp_string || tok(IDENT) || interpolation 555: res << e << str{ss} 556: end 557: res 558: end
# File lib/sass/scss/parser.rb, line 305 305: def ruleset 306: return unless rules = selector_sequence 307: block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset) 308: end
# File lib/sass/scss/parser.rb, line 60 60: def s(node) 61: while tok(S) || tok(CDC) || tok(CDO) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT)) 62: next unless c 63: process_comment c, node 64: c = nil 65: end 66: true 67: end
# File lib/sass/scss/parser.rb, line 731 731: def sass_script(*args) 732: parser = self.class.sass_script_parser.new(@scanner, @line, 733: @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0)) 734: result = parser.send(*args) 735: @line = parser.line 736: result 737: end
# File lib/sass/scss/parser.rb, line 393 393: def selector 394: return unless sel = _selector 395: sel.to_a 396: end
# File lib/sass/scss/parser.rb, line 398 398: def selector_comma_sequence 399: return unless sel = _selector 400: selectors = [sel] 401: while tok(/,/) 402: ws = str{ss} 403: selectors << expr!(:_selector) 404: selectors[1] = Selector::Sequence.new(["\n"] + selectors.last.members) if ws.include?("\n") 405: end 406: Selector::CommaSequence.new(selectors) 407: end
# File lib/sass/scss/parser.rb, line 377 377: def selector_sequence 378: if sel = tok(STATIC_SELECTOR) 379: return [sel] 380: end 381: 382: rules = [] 383: return unless v = selector 384: rules.concat v 385: 386: while tok(/,/) 387: rules << ',' << str {ss} 388: rules.concat expr!(:selector) 389: end 390: rules 391: end
# File lib/sass/scss/parser.rb, line 428 428: def simple_selector_sequence 429: # This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes- 430: return expr unless e = element_name || id_selector || class_selector || 431: attrib || negation || pseudo || parent_selector || interpolation_selector 432: res = [e] 433: 434: # The tok(/\*/) allows the "E*" hack 435: while v = element_name || id_selector || class_selector || 436: attrib || negation || pseudo || interpolation_selector || 437: (tok(/\*/) && Selector::Universal.new(nil)) 438: res << v 439: end 440: 441: if tok?(/&/) 442: begin 443: expected('"{"') 444: rescue Sass::SyntaxError => e 445: e.message << "\n\n" << In Sass 3, the parent selector & can only be used where element names are valid,since it could potentially be replaced by an element name. 446: raise e 447: end 448: end 449: 450: Selector::SimpleSequence.new(res) 451: end
# File lib/sass/scss/parser.rb, line 128 128: def special_directive(name) 129: sym = name.gsub('-', '_').to_sym 130: DIRECTIVES.include?(sym) && send("#{sym}_directive") 131: end
# File lib/sass/scss/parser.rb, line 69 69: def ss 70: nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT) 71: true 72: end
# File lib/sass/scss/parser.rb, line 74 74: def ss_comments(node) 75: while tok(S) || (c = tok(SINGLE_LINE_COMMENT)) || (c = tok(COMMENT)) 76: next unless c 77: process_comment c, node 78: c = nil 79: end 80: 81: true 82: end
# File lib/sass/scss/parser.rb, line 706 706: def str 707: @strs.push "" 708: yield 709: @strs.last 710: ensure 711: @strs.pop 712: end
# File lib/sass/scss/parser.rb, line 714 714: def str? 715: @strs.push "" 716: yield && @strs.last 717: ensure 718: @strs.pop 719: end
# File lib/sass/scss/parser.rb, line 55 55: def stylesheet 56: node = node(Sass::Tree::RootNode.new(@scanner.string)) 57: block_contents(node, :stylesheet) {s(node)} 58: end
# File lib/sass/scss/parser.rb, line 643 643: def term 644: unless e = tok(NUMBER) || 645: tok(URI) || 646: function || 647: interp_string || 648: tok(UNICODERANGE) || 649: tok(IDENT) || 650: tok(HEXCOLOR) || 651: interpolation 652: 653: return unless op = unary_operator 654: @expected = "number or function" 655: return [op, tok(NUMBER) || expr!(:function)] 656: end 657: e 658: end
# File lib/sass/scss/parser.rb, line 809 809: def tok(rx) 810: res = @scanner.scan(rx) 811: if res 812: @line += res.count("\n") 813: @expected = nil 814: if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT 815: @strs.each {|s| s << res} 816: end 817: end 818: 819: res 820: end
# File lib/sass/scss/parser.rb, line 767 767: def tok!(rx) 768: (t = tok(rx)) && (return t) 769: name = TOK_NAMES[rx] 770: 771: unless name 772: # Display basic regexps as plain old strings 773: string = rx.source.gsub(/\\(.)/, '\1') 774: name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect 775: end 776: 777: expected(name) 778: end
# File lib/sass/scss/parser.rb, line 758 758: def tok?(rx) 759: @scanner.match?(rx) 760: end
# File lib/sass/scss/parser.rb, line 301 301: def unary_operator 302: tok(/[+-]/) 303: end
# File lib/sass/scss/parser.rb, line 230 230: def use_css_import?; false; end
# File lib/sass/scss/parser.rb, line 594 594: def value! 595: space = !str {ss}.empty? 596: @use_property_exception ||= space || !tok?(IDENT) 597: 598: return true, Sass::Script::String.new("") if tok?(/\{/) 599: # This is a bit of a dirty trick: 600: # if the value is completely static, 601: # we don't parse it at all, and instead return a plain old string 602: # containing the value. 603: # This results in a dramatic speed increase. 604: if val = tok(STATIC_VALUE) 605: return space, Sass::Script::String.new(val.strip) 606: end 607: return space, sass_script(:parse) 608: end
# File lib/sass/scss/parser.rb, line 283 283: def variable 284: return unless tok(/\$/) 285: name = tok!(IDENT) 286: ss; tok!(/:/); ss 287: 288: expr = sass_script(:parse) 289: guarded = tok(DEFAULT) 290: node(Sass::Tree::VariableNode.new(name, expr, guarded)) 291: end
# File lib/sass/scss/parser.rb, line 151 151: def warn_directive 152: node(Sass::Tree::WarnNode.new(sass_script(:parse))) 153: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.