PageRenderTime 115ms CodeModel.GetById 17ms app.highlight 91ms RepoModel.GetById 1ms app.codeStats 0ms

/scalate-jruby/src/main/resources/haml-3.0.25/lib/sass/scss/parser.rb

http://github.com/scalate/scalate
Ruby | 855 lines | 656 code | 133 blank | 66 comment | 134 complexity | a21ec8dae92495ba3affbf42fbe72684 MD5 | raw file
  1require 'strscan'
  2require 'set'
  3
  4module Sass
  5  module SCSS
  6    # The parser for SCSS.
  7    # It parses a string of code into a tree of {Sass::Tree::Node}s.
  8    class Parser
  9      # @param str [String, StringScanner] The source document to parse.
 10      #   Note that `Parser` *won't* raise a nice error message if this isn't properly parsed;
 11      #   for that, you should use the higher-level {Sass::Engine} or {Sass::CSS}.
 12      # @param line [Fixnum] The line on which the source string appeared,
 13      #   if it's part of another document
 14      def initialize(str, line = 1)
 15        @template = str
 16        @line = line
 17        @strs = []
 18      end
 19
 20      # Parses an SCSS document.
 21      #
 22      # @return [Sass::Tree::RootNode] The root node of the document tree
 23      # @raise [Sass::SyntaxError] if there's a syntax error in the document
 24      def parse
 25        init_scanner!
 26        root = stylesheet
 27        expected("selector or at-rule") unless @scanner.eos?
 28        root
 29      end
 30
 31      # Parses an identifier with interpolation.
 32      # Note that this won't assert that the identifier takes up the entire input string;
 33      # it's meant to be used with `StringScanner`s as part of other parsers.
 34      #
 35      # @return [Array<String, Sass::Script::Node>, nil]
 36      #   The interpolated identifier, or nil if none could be parsed
 37      def parse_interp_ident
 38        init_scanner!
 39        interp_ident
 40      end
 41
 42      private
 43
 44      include Sass::SCSS::RX
 45
 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
 54
 55      def stylesheet
 56        node = node(Sass::Tree::RootNode.new(@scanner.string))
 57        block_contents(node, :stylesheet) {s(node)}
 58      end
 59
 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
 68
 69      def ss
 70        nil while tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
 71        true
 72      end
 73
 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
 83
 84      def whitespace
 85        return unless tok(S) || tok(SINGLE_LINE_COMMENT) || tok(COMMENT)
 86        ss
 87      end
 88
 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
100
101      DIRECTIVES = Set[:mixin, :include, :debug, :warn, :for, :while, :if, :else,
102        :extend, :import, :media, :charset]
103
104      def directive
105        return unless tok(/@/)
106        name = tok!(IDENT)
107        ss
108
109        if dir = special_directive(name)
110          return dir
111        end
112
113        # Most at-rules take expressions (e.g. @import),
114        # but some (e.g. @page) take selector-like arguments
115        val = str {break unless expr}
116        val ||= CssParser.new(@scanner, @line).parse_selector_string
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
127
128      def special_directive(name)
129        sym = name.gsub('-', '_').to_sym
130        DIRECTIVES.include?(sym) && send("#{sym}_directive")
131      end
132
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
139
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
146
147      def debug_directive
148        node(Sass::Tree::DebugNode.new(sass_script(:parse)))
149      end
150
151      def warn_directive
152        node(Sass::Tree::WarnNode.new(sass_script(:parse)))
153      end
154
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
171
172      def while_directive
173        expr = sass_script(:parse)
174        ss
175        block(node(Sass::Tree::WhileNode.new(expr)), :directive)
176      end
177
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        line = @line
184        ss
185
186        else_block(node) ||
187          begin
188            # Backtrack in case there are any comments we want to parse
189            @scanner.pos = pos
190            @line = line
191            node
192          end
193      end
194
195      def else_block(node)
196        return unless tok(/@else/)
197        ss
198        else_node = block(
199          Sass::Tree::IfNode.new((sass_script(:parse) if tok(/if/))),
200          :directive)
201        node.add_else(else_node)
202        pos = @scanner.pos
203        line = @line
204        ss
205
206        else_block(node) ||
207          begin
208            # Backtrack in case there are any comments we want to parse
209            @scanner.pos = pos
210            @line = line
211            node
212          end
213      end
214
215      def else_directive
216        raise Sass::SyntaxError.new(
217          "Invalid CSS: @else must come after @if", :line => @line)
218      end
219
220      def extend_directive
221        node(Sass::Tree::ExtendNode.new(expr!(:selector)))
222      end
223
224      def import_directive
225        values = []
226
227        loop do
228          values << expr!(:import_arg)
229          break if use_css_import? || !tok(/,\s*/)
230        end
231
232        return values
233      end
234
235      def import_arg
236        return unless arg = tok(STRING) || (uri = tok!(URI))
237        path = @scanner[1] || @scanner[2] || @scanner[3]
238        ss
239
240        media = str {media_query_list}.strip
241
242        if uri || path =~ /^http:\/\// || !media.strip.empty? || use_css_import?
243          return node(Sass::Tree::DirectiveNode.new("@import #{arg} #{media}".strip))
244        end
245
246        node(Sass::Tree::ImportNode.new(path.strip))
247      end
248
249      def use_css_import?; false; end
250
251      def media_directive
252        val = str {media_query_list}.strip
253        block(node(Sass::Tree::DirectiveNode.new("@media #{val}")), :directive)
254      end
255
256      # http://www.w3.org/TR/css3-mediaqueries/#syntax
257      def media_query_list
258        return unless media_query
259
260        ss
261        while tok(/,/)
262          ss; expr!(:media_query); ss
263        end
264
265        true
266      end
267
268      def media_query
269        if tok(/only|not/i)
270          ss
271          @expected = "media type (e.g. print, screen)"
272          tok!(IDENT)
273          ss
274        elsif !tok(IDENT) && !media_expr
275          return
276        end
277
278        ss
279        while tok(/and/i)
280          ss; expr!(:media_expr); ss
281        end
282
283        true
284      end
285
286      def media_expr
287        return unless tok(/\(/)
288        ss
289        @expected = "media feature (e.g. min-device-width, color)"
290        tok!(IDENT)
291        ss
292
293        if tok(/:/)
294          ss; expr!(:expr)
295        end
296        tok!(/\)/)
297        ss
298
299        true
300      end
301
302      def charset_directive
303        tok! STRING
304        name = @scanner[1] || @scanner[2]
305        ss
306        node(Sass::Tree::CharsetNode.new(name))
307      end
308
309      def variable
310        return unless tok(/\$/)
311        name = tok!(IDENT)
312        ss; tok!(/:/); ss
313
314        expr = sass_script(:parse)
315        guarded = tok(DEFAULT)
316        node(Sass::Tree::VariableNode.new(name, expr, guarded))
317      end
318
319      def operator
320        # Many of these operators (all except / and ,)
321        # are disallowed by the CSS spec,
322        # but they're included here for compatibility
323        # with some proprietary MS properties
324        str {ss if tok(/[\/,:.=]/)}
325      end
326
327      def unary_operator
328        tok(/[+-]/)
329      end
330
331      def ruleset
332        return unless rules = selector_sequence
333        block(node(Sass::Tree::RuleNode.new(rules.flatten.compact)), :ruleset)
334      end
335
336      def block(node, context)
337        node.has_children = true
338        tok!(/\{/)
339        block_contents(node, context)
340        tok!(/\}/)
341        node
342      end
343
344      # A block may contain declarations and/or rulesets
345      def block_contents(node, context)
346        block_given? ? yield : ss_comments(node)
347        node << (child = block_child(context))
348        while tok(/;/) || has_children?(child)
349          block_given? ? yield : ss_comments(node)
350          node << (child = block_child(context))
351        end
352        node
353      end
354
355      def block_child(context)
356        return variable || directive || ruleset if context == :stylesheet
357        variable || directive || declaration_or_ruleset
358      end
359
360      def has_children?(child_or_array)
361        return false unless child_or_array
362        return child_or_array.last.has_children if child_or_array.is_a?(Array)
363        return child_or_array.has_children
364      end
365
366      # This is a nasty hack, and the only place in the parser
367      # that requires backtracking.
368      # The reason is that we can't figure out if certain strings
369      # are declarations or rulesets with fixed finite lookahead.
370      # For example, "foo:bar baz baz baz..." could be either a property
371      # or a selector.
372      #
373      # To handle this, we simply check if it works as a property
374      # (which is the most common case)
375      # and, if it doesn't, try it as a ruleset.
376      #
377      # We could eke some more efficiency out of this
378      # by handling some easy cases (first token isn't an identifier,
379      # no colon after the identifier, whitespace after the colon),
380      # but I'm not sure the gains would be worth the added complexity.
381      def declaration_or_ruleset
382        pos = @scanner.pos
383        line = @line
384        old_use_property_exception, @use_property_exception =
385          @use_property_exception, false
386        begin
387          decl = declaration
388          unless decl && decl.has_children
389            # We want an exception if it's not there,
390            # but we don't want to consume if it is
391            tok!(/[;}]/) unless tok?(/[;}]/)
392          end
393          return decl
394        rescue Sass::SyntaxError => decl_err
395        end
396
397        @line = line
398        @scanner.pos = pos
399
400        begin
401          return ruleset
402        rescue Sass::SyntaxError => ruleset_err
403          raise @use_property_exception ? decl_err : ruleset_err
404        end
405      ensure
406        @use_property_exception = old_use_property_exception
407      end
408
409      def selector_sequence
410        if sel = tok(STATIC_SELECTOR)
411          return [sel]
412        end
413
414        rules = []
415        return unless v = selector
416        rules.concat v
417
418        while tok(/,/)
419          rules << ',' << str {ss}
420          rules.concat expr!(:selector)
421        end
422        rules
423      end
424
425      def selector
426        return unless sel = _selector
427        sel.to_a
428      end
429
430      def selector_comma_sequence
431        return unless sel = _selector
432        selectors = [sel]
433        while tok(/,/)
434          ws = str{ss}
435          selectors << expr!(:_selector)
436          selectors[-1] = Selector::Sequence.new(["\n"] + selectors.last.members) if ws.include?("\n")
437        end
438        Selector::CommaSequence.new(selectors)
439      end
440
441      def _selector
442        # The combinator here allows the "> E" hack
443        return unless val = combinator || simple_selector_sequence
444        nl = str{ss}.include?("\n")
445        res = []
446        res << val
447        res << "\n" if nl
448
449        while val = combinator || simple_selector_sequence
450          res << val
451          res << "\n" if str{ss}.include?("\n")
452        end
453        Selector::Sequence.new(res.compact)
454      end
455
456      def combinator
457        tok(PLUS) || tok(GREATER) || tok(TILDE)
458      end
459
460      def simple_selector_sequence
461        # This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
462        return expr unless e = element_name || id_selector || class_selector ||
463          attrib || negation || pseudo || parent_selector || interpolation_selector
464        res = [e]
465
466        # The tok(/\*/) allows the "E*" hack
467        while v = element_name || id_selector || class_selector ||
468            attrib || negation || pseudo || interpolation_selector ||
469            (tok(/\*/) && Selector::Universal.new(nil))
470          res << v
471        end
472
473        if tok?(/&/)
474          begin
475            expected('"{"')
476          rescue Sass::SyntaxError => e
477            e.message << "\n\n" << <<MESSAGE
478In Sass 3, the parent selector & can only be used where element names are valid,
479since it could potentially be replaced by an element name.
480MESSAGE
481            raise e
482          end
483        end
484
485        Selector::SimpleSequence.new(res)
486      end
487
488      def parent_selector
489        return unless tok(/&/)
490        Selector::Parent.new
491      end
492
493      def class_selector
494        return unless tok(/\./)
495        @expected = "class name"
496        Selector::Class.new(merge(expr!(:interp_ident)))
497      end
498
499      def id_selector
500        return unless tok(/#(?!\{)/)
501        @expected = "id name"
502        Selector::Id.new(merge(expr!(:interp_name)))
503      end
504
505      def element_name
506        return unless name = interp_ident || tok(/\*/) || (tok?(/\|/) && "")
507        if tok(/\|/)
508          @expected = "element name or *"
509          ns = name
510          name = interp_ident || tok!(/\*/)
511        end
512
513        if name == '*'
514          Selector::Universal.new(merge(ns))
515        else
516          Selector::Element.new(merge(name), merge(ns))
517        end
518      end
519
520      def interpolation_selector
521        return unless script = interpolation
522        Selector::Interpolation.new(script)
523      end
524
525      def attrib
526        return unless tok(/\[/)
527        ss
528        ns, name = attrib_name!
529        ss
530
531        if op = tok(/=/) ||
532            tok(INCLUDES) ||
533            tok(DASHMATCH) ||
534            tok(PREFIXMATCH) ||
535            tok(SUFFIXMATCH) ||
536            tok(SUBSTRINGMATCH)
537          @expected = "identifier or string"
538          ss
539          if val = tok(IDENT)
540            val = [val]
541          else
542            val = expr!(:interp_string)
543          end
544          ss
545        end
546        tok(/\]/)
547
548        Selector::Attribute.new(merge(name), merge(ns), op, merge(val))
549      end
550
551      def attrib_name!
552        if name_or_ns = interp_ident
553          # E, E|E
554          if tok(/\|(?!=)/)
555            ns = name_or_ns
556            name = interp_ident
557          else
558            name = name_or_ns
559          end
560        else
561          # *|E or |E
562          ns = [tok(/\*/) || ""]
563          tok!(/\|/)
564          name = expr!(:interp_ident)
565        end
566        return ns, name
567      end
568
569      def pseudo
570        return unless s = tok(/::?/)
571        @expected = "pseudoclass or pseudoelement"
572        name = expr!(:interp_ident)
573        if tok(/\(/)
574          ss
575          arg = expr!(:pseudo_expr)
576          tok!(/\)/)
577        end
578        Selector::Pseudo.new(s == ':' ? :class : :element, merge(name), merge(arg))
579      end
580
581      def pseudo_expr
582        return unless e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
583          interp_string || tok(IDENT) || interpolation
584        res = [e, str{ss}]
585        while e = tok(PLUS) || tok(/-/) || tok(NUMBER) ||
586            interp_string || tok(IDENT) || interpolation
587          res << e << str{ss}
588        end
589        res
590      end
591
592      def negation
593        return unless name = tok(NOT) || tok(MOZ_ANY)
594        ss
595        @expected = "selector"
596        sel = selector_comma_sequence
597        tok!(/\)/)
598        Selector::SelectorPseudoClass.new(name[1...-1], sel)
599      end
600
601      def declaration
602        # This allows the "*prop: val", ":prop: val", and ".prop: val" hacks
603        if s = tok(/[:\*\.]|\#(?!\{)/)
604          @use_property_exception = s !~ /[\.\#]/
605          name = [s, str{ss}, *expr!(:interp_ident)]
606        else
607          return unless name = interp_ident
608          name = [name] if name.is_a?(String)
609        end
610        if comment = tok(COMMENT)
611          name << comment
612        end
613        ss
614
615        tok!(/:/)
616        space, value = value!
617        ss
618        require_block = tok?(/\{/)
619
620        node = node(Sass::Tree::PropNode.new(name.flatten.compact, value, :new))
621
622        return node unless require_block
623        nested_properties! node, space
624      end
625
626      def value!
627        space = !str {ss}.empty?
628        @use_property_exception ||= space || !tok?(IDENT)
629
630        return true, Sass::Script::String.new("") if tok?(/\{/)
631        # This is a bit of a dirty trick:
632        # if the value is completely static,
633        # we don't parse it at all, and instead return a plain old string
634        # containing the value.
635        # This results in a dramatic speed increase.
636        if val = tok(STATIC_VALUE)
637          return space, Sass::Script::String.new(val.strip)
638        end
639        return space, sass_script(:parse)
640      end
641
642      def plain_value
643        return unless tok(/:/)
644        space = !str {ss}.empty?
645        @use_property_exception ||= space || !tok?(IDENT)
646
647        expression = expr
648        expression << tok(IMPORTANT) if expression
649        # expression, space, value
650        return expression, space, expression || [""]
651      end
652
653      def nested_properties!(node, space)
654        raise Sass::SyntaxError.new(<<MESSAGE, :line => @line) unless space
655Invalid CSS: a space is required between a property and its definition
656when it has other properties nested beneath it.
657MESSAGE
658
659        @use_property_exception = true
660        @expected = 'expression (e.g. 1px, bold) or "{"'
661        block(node, :property)
662      end
663
664      def expr
665        return unless t = term
666        res = [t, str{ss}]
667
668        while (o = operator) && (t = term)
669          res << o << t << str{ss}
670        end
671
672        res
673      end
674
675      def term
676        unless e = tok(NUMBER) ||
677            tok(URI) ||
678            function ||
679            tok(STRING) ||
680            tok(UNICODERANGE) ||
681            tok(IDENT) ||
682            tok(HEXCOLOR)
683
684          return unless op = unary_operator
685          @expected = "number or function"
686          return [op, tok(NUMBER) || expr!(:function)]
687        end
688        e
689      end
690
691      def function
692        return unless name = tok(FUNCTION)
693        if name == "expression(" || name == "calc("
694          str, _ = Haml::Shared.balance(@scanner, ?(, ?), 1)
695          [name, str]
696        else
697          [name, str{ss}, expr, tok!(/\)/)]
698        end
699      end
700
701      def interpolation
702        return unless tok(INTERP_START)
703        sass_script(:parse_interpolated)
704      end
705
706      def interp_string
707        _interp_string(:double) || _interp_string(:single)
708      end
709
710      def _interp_string(type)
711        return unless start = tok(Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, false]])
712        res = [start]
713
714        mid_re = Sass::Script::Lexer::STRING_REGULAR_EXPRESSIONS[[type, true]]
715        # @scanner[2].empty? means we've started an interpolated section
716        while @scanner[2] == '#{'
717          @scanner.pos -= 2 # Don't consume the #{
718          res.last.slice!(-2..-1)
719          res << expr!(:interpolation) << tok(mid_re)
720        end
721        res
722      end
723
724      def interp_ident(start = IDENT)
725        return unless val = tok(start) || interpolation
726        res = [val]
727        while val = tok(NAME) || interpolation
728          res << val
729        end
730        res
731      end
732
733      def interp_name
734        interp_ident NAME
735      end
736
737      def str
738        @strs.push ""
739        yield
740        @strs.last
741      ensure
742        @strs.pop
743      end
744
745      def str?
746        @strs.push ""
747        yield && @strs.last
748      ensure
749        @strs.pop
750      end
751
752      def node(node)
753        node.line = @line
754        node
755      end
756
757      @sass_script_parser = Class.new(Sass::Script::Parser)
758      @sass_script_parser.send(:include, ScriptParser)
759      # @private
760      def self.sass_script_parser; @sass_script_parser; end
761
762      def sass_script(*args)
763        parser = self.class.sass_script_parser.new(@scanner, @line,
764          @scanner.pos - (@scanner.string[0...@scanner.pos].rindex("\n") || 0))
765        result = parser.send(*args)
766        @line = parser.line
767        result
768      end
769
770      def merge(arr)
771        arr && Haml::Util.merge_adjacent_strings([arr].flatten)
772      end
773
774      EXPR_NAMES = {
775        :media_query => "media query (e.g. print, screen, print and screen)",
776        :media_expr => "media expression (e.g. (min-device-width: 800px)))",
777        :pseudo_expr => "expression (e.g. fr, 2n+1)",
778        :interp_ident => "identifier",
779        :interp_name => "identifier",
780        :expr => "expression (e.g. 1px, bold)",
781        :selector_comma_sequence => "selector",
782        :simple_selector_sequence => "selector",
783        :import_arg => "file to import (string or url())",
784      }
785
786      TOK_NAMES = Haml::Util.to_hash(
787        Sass::SCSS::RX.constants.map {|c| [Sass::SCSS::RX.const_get(c), c.downcase]}).
788        merge(IDENT => "identifier", /[;}]/ => '";"')
789
790      def tok?(rx)
791        @scanner.match?(rx)
792      end
793
794      def expr!(name)
795        (e = send(name)) && (return e)
796        expected(EXPR_NAMES[name] || name.to_s)
797      end
798
799      def tok!(rx)
800        (t = tok(rx)) && (return t)
801        name = TOK_NAMES[rx]
802
803        unless name
804          # Display basic regexps as plain old strings
805          string = rx.source.gsub(/\\(.)/, '\1')
806          name = rx.source == Regexp.escape(string) ? string.inspect : rx.inspect
807        end
808
809        expected(name)
810      end
811
812      def expected(name)
813        self.class.expected(@scanner, @expected || name, @line)
814      end
815
816      # @private
817      def self.expected(scanner, expected, line)
818        pos = scanner.pos
819
820        after = scanner.string[0...pos]
821        # Get rid of whitespace between pos and the last token,
822        # but only if there's a newline in there
823        after.gsub!(/\s*\n\s*$/, '')
824        # Also get rid of stuff before the last newline
825        after.gsub!(/.*\n/, '')
826        after = "..." + after[-15..-1] if after.size > 18
827
828        was = scanner.rest.dup
829        # Get rid of whitespace between pos and the next token,
830        # but only if there's a newline in there
831        was.gsub!(/^\s*\n\s*/, '')
832        # Also get rid of stuff after the next newline
833        was.gsub!(/\n.*/, '')
834        was = was[0...15] + "..." if was.size > 18
835
836        raise Sass::SyntaxError.new(
837          "Invalid CSS after \"#{after}\": expected #{expected}, was \"#{was}\"",
838          :line => line)
839      end
840
841      def tok(rx)
842        res = @scanner.scan(rx)
843        if res
844          @line += res.count("\n")
845          @expected = nil
846          if !@strs.empty? && rx != COMMENT && rx != SINGLE_LINE_COMMENT
847            @strs.each {|s| s << res}
848          end
849        end
850
851        res
852      end
853    end
854  end
855end