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.