Class | Sass::SCSS::Parser |
In: |
lib/sass/scss/parser.rb
|
Parent: | Object |
The parser for SCSS. It parses a string of code into a tree of {Sass::Tree::Node}s.
DIRECTIVES | = | Set[:mixin, :include, :debug, :warn, :for, :while, :if, :extend, :import, :media] |
EXPR_NAMES | = | { :media_query => "media query (e.g. print, screen, print and screen)", :media_expr => "media expression (e.g. (min-device-width: 800px)))", :pseudo_expr => "expression (e.g. fr, 2n+1)", :interp_ident => "identifier", :interp_name => "identifier", :expr => "expression (e.g. 1px, bold)", :_selector => "selector", :simple_selector_sequence => "selector", } |
TOK_NAMES | = | Haml::Util.to_hash( Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}). merge(IDENT => "identifier", /[;}]/ => '";"') |
@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 762 762: def self.expected(scanner, expected, line) 763: pos = scanner.pos 764: 765: after = scanner.string[0...pos] 766: # Get rid of whitespace between pos and the last token, 767: # but only if there's a newline in there 768: after.gsub!(/\s*\n\s*$/, '') 769: # Also get rid of stuff before the last newline 770: after.gsub!(/.*\n/, '') 771: after = "..." + after[-15..-1] if after.size > 18 772: 773: was = scanner.rest.dup 774: # Get rid of whitespace between pos and the next token, 775: # but only if there's a newline in there 776: was.gsub!(/^\s*\n\s*/, '') 777: # Also get rid of stuff after the next newline 778: was.gsub!(/\n.*/, '') 779: was = was[0...15] + "..." if was.size > 18 780: 781: raise Sass::SyntaxError.new( 782: "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"", 783: :line => line) 784: end
@private
# File lib/sass/scss/parser.rb, line 706 706: def self.sass_script_parser; @sass_script_parser; 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<String, Sass::Script::Node>, nil]
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 656 656: def _interp_string(type) 657: return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]]) 658: res = [start] 659: 660: mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, true]] 661: # @scanner[2].empty? means we've started an interpolated section 662: while @scanner[2] == '#{' 663: @scanner.pos -= 2 # Don't consume the #{ 664: res.last.slice!(-2..-1) 665: res << expr!(:interpolation) << tok(mid_re) 666: end 667: res 668: end
# File lib/sass/scss/parser.rb, line 384 384: def _selector 385: # The combinator here allows the "> E" hack 386: return unless val = combinator || simple_selector_sequence 387: nl = str{ss}.include?("\n") 388: res = [] 389: res << val 390: res << "\n" if nl 391: 392: while val = combinator || simple_selector_sequence 393: res << val 394: res << "\n" if str{ss}.include?("\n") 395: end 396: Selector::Sequence.new(res.compact) 397: end
# File lib/sass/scss/parser.rb, line 469 469: def attrib 470: return unless tok(/\[/) 471: ss 472: ns, name = attrib_name! 473: ss 474: 475: if op = tok(/=/) || 476: tok(INCLUDES) || 477: tok(DASHMATCH) || 478: tok(PREFIXMATCH) || 479: tok(SUFFIXMATCH) || 480: tok(SUBSTRINGMATCH) 481: @expected = "identifier or string" 482: ss 483: if val = tok(IDENT) 484: val = [val] 485: else 486: val = expr!(:interp_string) 487: end 488: ss 489: end 490: tok(/\]/) 491: 492: Selector::Attribute.new(merge(name), merge(ns), op, merge(val)) 493: end
# File lib/sass/scss/parser.rb, line 495 495: def attrib_name! 496: if name_or_ns = interp_ident 497: # E, E|E 498: if tok(/\|(?!=)/) 499: ns = name_or_ns 500: name = interp_ident 501: else 502: name = name_or_ns 503: end 504: else 505: # *|E or |E 506: ns = [tok(/\*/) || ""] 507: tok!(/\|/) 508: name = expr!(:interp_ident) 509: end 510: return ns, name 511: end
# File lib/sass/scss/parser.rb, line 296 296: def block(node, context) 297: node.has_children = true 298: tok!(/\{/) 299: block_contents(node, context) 300: tok!(/\}/) 301: node 302: end
# File lib/sass/scss/parser.rb, line 315 315: def block_child(context) 316: return variable || directive || ruleset if context == :stylesheet 317: variable || directive || declaration_or_ruleset 318: end
A block may contain declarations and/or rulesets
# File lib/sass/scss/parser.rb, line 305 305: def block_contents(node, context) 306: block_given? ? yield : ss_comments(node) 307: node << (child = block_child(context)) 308: while tok(/;/) || (child && child.has_children) 309: block_given? ? yield : ss_comments(node) 310: node << (child = block_child(context)) 311: end 312: node 313: end
# File lib/sass/scss/parser.rb, line 437 437: def class_selector 438: return unless tok(/\./) 439: @expected = "class name" 440: Selector::Class.new(merge(expr!(:interp_ident))) 441: end
# File lib/sass/scss/parser.rb, line 399 399: def combinator 400: tok(PLUS) || tok(GREATER) || tok(TILDE) 401: 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 545 545: def declaration 546: # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks 547: if s = tok(/[:\*\.]|\#(?!\{)/) 548: @use_property_exception = s !~ /[\.\#]/ 549: name = [s, str{ss}, *expr!(:interp_ident)] 550: else 551: return unless name = interp_ident 552: name = [name] if name.is_a?(String) 553: end 554: if comment = tok(COMMENT) 555: name << comment 556: end 557: ss 558: 559: tok!(/:/) 560: space, value = value! 561: ss 562: require_block = tok?(/\{/) 563: 564: node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new)) 565: 566: return node unless require_block 567: nested_properties! node, space 568: 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 335 335: def declaration_or_ruleset 336: pos = @scanner.pos 337: line = @line 338: old_use_property_exception, @use_property_exception = 339: @use_property_exception, false 340: begin 341: decl = declaration 342: unless decl && decl.has_children 343: # We want an exception if it's not there, 344: # but we don't want to consume if it is 345: tok!(/[;}]/) unless tok?(/[;}]/) 346: end 347: return decl 348: rescue Sass::SyntaxError => decl_err 349: end 350: 351: @line = line 352: @scanner.pos = pos 353: 354: begin 355: return ruleset 356: rescue Sass::SyntaxError => ruleset_err 357: raise @use_property_exception ? decl_err : ruleset_err 358: end 359: ensure 360: @use_property_exception = old_use_property_exception 361: 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 449 449: def element_name 450: return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "") 451: if tok(/\|/) 452: @expected = "element name or *" 453: ns = name 454: name = interp_ident || tok!(/\*/) 455: end 456: 457: if name == '*' 458: Selector::Universal.new(merge(ns)) 459: else 460: Selector::Element.new(merge(name), merge(ns)) 461: end 462: end
# File lib/sass/scss/parser.rb, line 186 186: def else_block(node) 187: return node unless tok(/@else/) 188: ss 189: else_node = block( 190: Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))), 191: :directive) 192: node.add_else(else_node) 193: ss 194: else_block(node) 195: end
# File lib/sass/scss/parser.rb, line 757 757: def expected(name) 758: self.class.expected(@scanner, @expected || name, @line) 759: end
# File lib/sass/scss/parser.rb, line 609 609: def expr 610: return unless t = term 611: res = [t, str{ss}] 612: 613: while (o = operator) && (t = term) 614: res << o << t << str{ss} 615: end 616: 617: res 618: end
# File lib/sass/scss/parser.rb, line 739 739: def expr!(name) 740: (e = send(name)) && (return e) 741: expected(EXPR_NAMES[name] || name.to_s) 742: end
# File lib/sass/scss/parser.rb, line 197 197: def extend_directive 198: node(Sass::Tree::ExtendNode.new(expr!(:selector))) 199: 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 637 637: def function 638: return unless name = tok(FUNCTION) 639: if name == "expression(" || name == "calc(" 640: str, _ = Haml::Shared.balance(@scanner, ?(, ?), 1) 641: [name, str] 642: else 643: [name, str{ss}, expr, tok!(/\)/)] 644: end 645: end
# File lib/sass/scss/parser.rb, line 443 443: def id_selector 444: return unless tok(/#(?!\{)/) 445: @expected = "id name" 446: Selector::Id.new(merge(expr!(:interp_name))) 447: 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: ss 183: else_block(node) 184: end
# File lib/sass/scss/parser.rb, line 201 201: def import_directive 202: @expected = "string or url()" 203: arg = tok(STRING) || tok!(URI) 204: path = @scanner[1] || @scanner[2] || @scanner[3] 205: ss 206: 207: media = str {media_query_list}.strip 208: 209: if !media.strip.empty? || use_css_import? 210: return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip)) 211: end 212: 213: node(Sass::Tree::ImportNode.new(path.strip)) 214: 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 670 670: def interp_ident(start = IDENT) 671: return unless val = tok(start) || interpolation 672: res = [val] 673: while val = tok(NAME) || interpolation 674: res << val 675: end 676: res 677: end
# File lib/sass/scss/parser.rb, line 652 652: def interp_string 653: _interp_string(:double) || _interp_string(:single) 654: end
# File lib/sass/scss/parser.rb, line 647 647: def interpolation 648: return unless tok(INTERP_START) 649: sass_script(:parse_interpolated) 650: end
# File lib/sass/scss/parser.rb, line 464 464: def interpolation_selector 465: return unless script = interpolation 466: Selector::Interpolation.new(script) 467: end
# File lib/sass/scss/parser.rb, line 218 218: def media_directive 219: val = str {media_query_list}.strip 220: block(node(Sass::Tree::DirectiveNode.new("@media #{val}")), :directive) 221: end
# File lib/sass/scss/parser.rb, line 253 253: def media_expr 254: return unless tok(/\(/) 255: ss 256: @expected = "media feature (e.g. min-device-width, color)" 257: tok!(IDENT) 258: ss 259: 260: if tok(/:/) 261: ss; expr!(:expr) 262: end 263: tok!(/\)/) 264: ss 265: 266: true 267: end
# File lib/sass/scss/parser.rb, line 235 235: def media_query 236: if tok(/only|not/i) 237: ss 238: @expected = "media type (e.g. print, screen)" 239: tok!(IDENT) 240: ss 241: elsif !tok(IDENT) && !media_expr 242: return 243: end 244: 245: ss 246: while tok(/and/i) 247: ss; expr!(:media_expr); ss 248: end 249: 250: true 251: end
www.w3.org/TR/css3-mediaqueries/#syntax
# File lib/sass/scss/parser.rb, line 224 224: def media_query_list 225: return unless media_query 226: 227: ss 228: while tok(/,/) 229: ss; expr!(:media_query); ss 230: end 231: 232: true 233: end
# File lib/sass/scss/parser.rb, line 716 716: def merge(arr) 717: arr && Haml::Util.merge_adjacent_strings([arr].flatten) 718: 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 536 536: def negation 537: return unless tok(NOT) 538: ss 539: @expected = "selector" 540: sel = element_name || id_selector || class_selector || attrib || expr!(:pseudo) 541: tok!(/\)/) 542: Selector::Negation.new(sel) 543: end
# File lib/sass/scss/parser.rb, line 597 597: def nested_properties!(node, space) 598: raise Sass::SyntaxError.new("Invalid CSS: a space is required between a property and its definition\nwhen it has other properties nested beneath it.\n", :line => @line) unless space 599: 600: @use_property_exception = true 601: @expected = 'expression (e.g. 1px, bold) or "{"' 602: block(node, :property) 603: end
# File lib/sass/scss/parser.rb, line 698 698: def node(node) 699: node.line = @line 700: node 701: end
# File lib/sass/scss/parser.rb, line 279 279: def operator 280: # Many of these operators (all except / and ,) 281: # are disallowed by the CSS spec, 282: # but they're included here for compatibility 283: # with some proprietary MS properties 284: str {ss if tok(/[\/,:.=]/)} 285: end
# File lib/sass/scss/parser.rb, line 432 432: def parent_selector 433: return unless tok(/&/) 434: Selector::Parent.new 435: end
# File lib/sass/scss/parser.rb, line 586 586: def plain_value 587: return unless tok(/:/) 588: space = !str {ss}.empty? 589: @use_property_exception ||= space || !tok?(IDENT) 590: 591: expression = expr 592: expression << tok(IMPORTANT) if expression 593: # expression, space, value 594: return expression, space, expression || [""] 595: 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 513 513: def pseudo 514: return unless s = tok(/::?/) 515: @expected = "pseudoclass or pseudoelement" 516: name = expr!(:interp_ident) 517: if tok(/\(/) 518: ss 519: arg = expr!(:pseudo_expr) 520: tok!(/\)/) 521: end 522: Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg)) 523: end
# File lib/sass/scss/parser.rb, line 525 525: def pseudo_expr 526: return unless e = tok(PLUS) || tok(/-/) || tok(NUMBER) || 527: interp_string || tok(IDENT) || interpolation 528: res = [e, str{ss}] 529: while e = tok(PLUS) || tok(/-/) || tok(NUMBER) || 530: interp_string || tok(IDENT) || interpolation 531: res << e << str{ss} 532: end 533: res 534: end
# File lib/sass/scss/parser.rb, line 291 291: def ruleset 292: return unless rules = selector_sequence 293: block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset) 294: 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 708 708: def sass_script(*args) 709: parser = self.class.sass_script_parser.new(@scanner, @line, 710: @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0)) 711: result = parser.send(*args) 712: @line = parser.line 713: result 714: end
# File lib/sass/scss/parser.rb, line 379 379: def selector 380: return unless sel = _selector 381: sel.to_a 382: end
# File lib/sass/scss/parser.rb, line 363 363: def selector_sequence 364: if sel = tok(STATIC_SELECTOR) 365: return [sel] 366: end 367: 368: rules = [] 369: return unless v = selector 370: rules.concat v 371: 372: while tok(/,/) 373: rules << ',' << str {ss} 374: rules.concat expr!(:selector) 375: end 376: rules 377: end
# File lib/sass/scss/parser.rb, line 403 403: def simple_selector_sequence 404: # This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes- 405: return expr unless e = element_name || id_selector || class_selector || 406: attrib || negation || pseudo || parent_selector || interpolation_selector 407: res = [e] 408: 409: # The tok(/\*/) allows the "E*" hack 410: while v = element_name || id_selector || class_selector || 411: attrib || negation || pseudo || interpolation_selector || 412: (tok(/\*/) && Selector::Universal.new(nil)) 413: res << v 414: end 415: 416: if tok?(/&/) 417: begin 418: expected('"{"') 419: rescue Sass::SyntaxError => e 420: e.message << "\n\n" << "In Sass 3, the parent selector & can only be used where element names are valid,\nsince it could potentially be replaced by an element name.\n" 421: raise e 422: end 423: end 424: 425: Selector::SimpleSequence.new(res) 426: 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 683 683: def str 684: @strs.push "" 685: yield 686: @strs.last 687: ensure 688: @strs.pop 689: end
# File lib/sass/scss/parser.rb, line 691 691: def str? 692: @strs.push "" 693: yield && @strs.last 694: ensure 695: @strs.pop 696: 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 620 620: def term 621: unless e = tok(NUMBER) || 622: tok(URI) || 623: function || 624: interp_string || 625: tok(UNICODERANGE) || 626: tok(IDENT) || 627: tok(HEXCOLOR) || 628: interpolation 629: 630: return unless op = unary_operator 631: @expected = "number or function" 632: return [op, tok(NUMBER) || expr!(:function)] 633: end 634: e 635: end
# File lib/sass/scss/parser.rb, line 786 786: def tok(rx) 787: res = @scanner.scan(rx) 788: if res 789: @line += res.count("\n") 790: @expected = nil 791: if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT 792: @strs.each {|s| s << res} 793: end 794: end 795: 796: res 797: end
# File lib/sass/scss/parser.rb, line 744 744: def tok!(rx) 745: (t = tok(rx)) && (return t) 746: name = TOK_NAMES[rx] 747: 748: unless name 749: # Display basic regexps as plain old strings 750: string = rx.source.gsub(/\\(.)/, '\1') 751: name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect 752: end 753: 754: expected(name) 755: end
# File lib/sass/scss/parser.rb, line 570 570: def value! 571: space = !str {ss}.empty? 572: @use_property_exception ||= space || !tok?(IDENT) 573: 574: return true, Sass::Script::String.new("") if tok?(/\{/) 575: # This is a bit of a dirty trick: 576: # if the value is completely static, 577: # we don't parse it at all, and instead return a plain old string 578: # containing the value. 579: # This results in a dramatic speed increase. 580: if val = tok(STATIC_VALUE) 581: return space, Sass::Script::String.new(val.strip) 582: end 583: return space, sass_script(:parse) 584: end
# File lib/sass/scss/parser.rb, line 269 269: def variable 270: return unless tok(/\$/) 271: name = tok!(IDENT) 272: ss; tok!(/:/); ss 273: 274: expr = sass_script(:parse) 275: guarded = tok(DEFAULT) 276: node(Sass::Tree::VariableNode.new(name, expr, guarded)) 277: end
# File lib/sass/scss/parser.rb, line 151 151: def warn_directive 152: node(Sass::Tree::WarnNode.new(sass_script(:parse))) 153: end
# File lib/sass/scss/parser.rb, line 172 172: def while_directive 173: expr = sass_script(:parse) 174: ss 175: block(node(Sass::Tree::WhileNode.new(expr)), :directive) 176: end