This class handles the parsing and compilation of the Sass template. Example usage:
template = File.load('stylesheets/sassy.sass')
sass_engine = Sass::Engine.new(template)
output = sass_engine.render
puts output
The character that begins a CSS property.
The character that designates that a property should be assigned to a SassScript expression.
The character that designates the beginning of a comment, either Sass or CSS.
The character that follows the general COMMENT_CHAR and designates a Sass comment, which is not output as a CSS comment.
The character that follows the general COMMENT_CHAR and designates a CSS comment, which is embedded in the CSS document.
The character used to denote a compiler directive.
Designates a non-parsed rule.
Designates block as mixin definition rather than CSS rules to output
Includes named mixin declared using MIXIN_DEFINITION_CHAR
The regex that matches properties of the form `name: prop`.
The regex that matches and extracts data from properties of the form `name: prop`.
The regex that matches and extracts data from properties of the form `:name prop`.
The default options for Sass::Engine. @api public
@param template [String] The Sass template.
This template can be encoded using any encoding
that can be converted to Unicode.
If the template contains an `@charset` declaration,
that overrides the Ruby encoding
(see {file:SASS_REFERENCE.md#encodings the encoding documentation})
@param options [{Symbol => Object}] An options hash;
see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
# File lib/sass/engine.rb, line 143
143: def initialize(template, options={})
144: @options = DEFAULT_OPTIONS.merge(options.reject {|k, v| v.nil?})
145: @template = template
146:
147: # Support both, because the docs said one and the other actually worked
148: # for quite a long time.
149: @options[:line_comments] ||= @options[:line_numbers]
150:
151: # Backwards compatibility
152: @options[:property_syntax] ||= @options[:attribute_syntax]
153: case @options[:property_syntax]
154: when :alternate; @options[:property_syntax] = :new
155: when :normal; @options[:property_syntax] = :old
156: end
157: end
It’s important that this have strings (at least) at the beginning, the end, and between each Script::Node.
@private
# File lib/sass/engine.rb, line 689
689: def self.parse_interp(text, line, offset, options)
690: res = []
691: rest = Haml::Shared.handle_interpolation text do |scan|
692: escapes = scan[2].size
693: res << scan.matched[0...2 - escapes]
694: if escapes % 2 == 1
695: res << "\\" * (escapes - 1) << '#{'
696: else
697: res << "\\" * [0, escapes - 1].max
698: res << Script::Parser.new(
699: scan, line, offset + scan.pos - scan.matched_size, options).
700: parse_interpolated
701: end
702: end
703: res << rest
704: end
Render the template to CSS.
@return [String] The CSS @raise [Sass::SyntaxError] if there’s an error in the document @raise [Encoding::UndefinedConversionError] if the source encoding
cannot be converted to UTF-8
@raise [ArgumentError] if the document uses an unknown encoding with `@charset`
# File lib/sass/engine.rb, line 166
166: def render
167: return _render unless @options[:quiet]
168: Haml::Util.silence_haml_warnings {_render}
169: end
Returns the original encoding of the document, or `nil` under Ruby 1.8.
@return [Encoding, nil] @raise [Encoding::UndefinedConversionError] if the source encoding
cannot be converted to UTF-8
@raise [ArgumentError] if the document uses an unknown encoding with `@charset`
# File lib/sass/engine.rb, line 188
188: def source_encoding
189: check_encoding!
190: @original_encoding
191: end
Parses the document into its parse tree.
@return [Sass::Tree::Node] The root of the parse tree. @raise [Sass::SyntaxError] if there’s an error in the document
# File lib/sass/engine.rb, line 176
176: def to_tree
177: return _to_tree unless @options[:quiet]
178: Haml::Util.silence_haml_warnings {_to_tree}
179: end
# File lib/sass/engine.rb, line 195
195: def _render
196: rendered = _to_tree.render
197: return rendered if ruby1_8?
198: return rendered.encode(source_encoding)
199: end
# File lib/sass/engine.rb, line 201
201: def _to_tree
202: check_encoding!
203:
204: if @options[:syntax] == :scss
205: root = Sass::SCSS::Parser.new(@template).parse
206: else
207: root = Tree::RootNode.new(@template)
208: append_children(root, tree(tabulate(@template)).first, true)
209: end
210:
211: root.options = @options
212: root
213: rescue SyntaxError => e
214: e.modify_backtrace(:filename => @options[:filename], :line => @line)
215: e.sass_template = @template
216: raise e
217: end
# File lib/sass/engine.rb, line 335
335: def append_children(parent, children, root)
336: continued_rule = nil
337: continued_comment = nil
338: children.each do |line|
339: child = build_tree(parent, line, root)
340:
341: if child.is_a?(Tree::RuleNode) && child.continued?
342: raise SyntaxError.new("Rules can't end in commas.",
343: :line => child.line) unless child.children.empty?
344: if continued_rule
345: continued_rule.add_rules child
346: else
347: continued_rule = child
348: end
349: next
350: end
351:
352: if continued_rule
353: raise SyntaxError.new("Rules can't end in commas.",
354: :line => continued_rule.line) unless child.is_a?(Tree::RuleNode)
355: continued_rule.add_rules child
356: continued_rule.children = child.children
357: continued_rule, child = nil, continued_rule
358: end
359:
360: if child.is_a?(Tree::CommentNode) && child.silent
361: if continued_comment &&
362: child.line == continued_comment.line +
363: continued_comment.value.count("\n") + 1
364: continued_comment.value << "\n" << child.value
365: next
366: end
367:
368: continued_comment = child
369: end
370:
371: check_for_no_children(child)
372: validate_and_append_child(parent, child, line, root)
373: end
374:
375: raise SyntaxError.new("Rules can't end in commas.",
376: :line => continued_rule.line) if continued_rule
377:
378: parent
379: end
# File lib/sass/engine.rb, line 318
318: def build_tree(parent, line, root = false)
319: @line = line.index
320: node_or_nodes = parse_line(parent, line, root)
321:
322: Array(node_or_nodes).each do |node|
323: # Node is a symbol if it's non-outputting, like a variable assignment
324: next unless node.is_a? Tree::Node
325:
326: node.line = line.index
327: node.filename = line.filename
328:
329: append_children(node, line.children, false)
330: end
331:
332: node_or_nodes
333: end
# File lib/sass/engine.rb, line 219
219: def check_encoding!
220: return if @checked_encoding
221: @checked_encoding = true
222: @template, @original_encoding = check_sass_encoding(@template) do |msg, line|
223: raise Sass::SyntaxError.new(msg, :line => line)
224: end
225: end
# File lib/sass/engine.rb, line 390
390: def check_for_no_children(node)
391: return unless node.is_a?(Tree::RuleNode) && node.children.empty?
392: Haml::Util.haml_warn(WARNING on line #{node.line}#{" of #{node.filename}" if node.filename}:This selector doesn't have any properties and will not be rendered..strip)
393: end
# File lib/sass/engine.rb, line 661
661: def format_comment_text(text, silent)
662: content = text.split("\n")
663:
664: if content.first && content.first.strip.empty?
665: removed_first = true
666: content.shift
667: end
668:
669: return silent ? "//" : "/* */" if content.empty?
670: content.last.gsub!(%{ ?\*/ *$}, '')
671: content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l}
672: content.first.gsub!(/^ /, '') unless removed_first
673: if silent
674: "//" + content.join("\n//")
675: else
676: # The #gsub fixes the case of a trailing */
677: "/*" + content.join("\n *").gsub(/ \*\Z/, '') + " */"
678: end
679: end
# File lib/sass/engine.rb, line 494
494: def parse_comment(line)
495: if line[1] == CSS_COMMENT_CHAR || line[1] == SASS_COMMENT_CHAR
496: silent = line[1] == SASS_COMMENT_CHAR
497: Tree::CommentNode.new(
498: format_comment_text(line[2..1], silent),
499: silent)
500: else
501: Tree::RuleNode.new(parse_interp(line))
502: end
503: end
# File lib/sass/engine.rb, line 505
505: def parse_directive(parent, line, root)
506: directive, whitespace, value = line.text[1..1].split(/(\s+)/, 2)
507: offset = directive.size + whitespace.size + 1 if whitespace
508:
509: # If value begins with url( or ",
510: # it's a CSS @import rule and we don't want to touch it.
511: if directive == "import"
512: parse_import(line, value)
513: elsif directive == "mixin"
514: parse_mixin_definition(line)
515: elsif directive == "include"
516: parse_mixin_include(line, root)
517: elsif directive == "for"
518: parse_for(line, root, value)
519: elsif directive == "else"
520: parse_else(parent, line, value)
521: elsif directive == "while"
522: raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
523: Tree::WhileNode.new(parse_script(value, :offset => offset))
524: elsif directive == "if"
525: raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
526: Tree::IfNode.new(parse_script(value, :offset => offset))
527: elsif directive == "debug"
528: raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
529: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
530: :line => @line + 1) unless line.children.empty?
531: offset = line.offset + line.text.index(value).to_i
532: Tree::DebugNode.new(parse_script(value, :offset => offset))
533: elsif directive == "extend"
534: raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
535: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
536: :line => @line + 1) unless line.children.empty?
537: offset = line.offset + line.text.index(value).to_i
538: Tree::ExtendNode.new(parse_interp(value, offset))
539: elsif directive == "warn"
540: raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
541: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
542: :line => @line + 1) unless line.children.empty?
543: offset = line.offset + line.text.index(value).to_i
544: Tree::WarnNode.new(parse_script(value, :offset => offset))
545: else
546: Tree::DirectiveNode.new(line.text)
547: end
548: end
# File lib/sass/engine.rb, line 574
574: def parse_else(parent, line, text)
575: previous = parent.children.last
576: raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
577:
578: if text
579: if text !~ /^if\s+(.+)/
580: raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.")
581: end
582: expr = parse_script($1, :offset => line.offset + line.text.index($1))
583: end
584:
585: node = Tree::IfNode.new(expr)
586: append_children(node, line.children, false)
587: previous.add_else node
588: nil
589: end
# File lib/sass/engine.rb, line 550
550: def parse_for(line, root, text)
551: var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
552:
553: if var.nil? # scan failed, try to figure out why for error message
554: if text !~ /^[^\s]+/
555: expected = "variable name"
556: elsif text !~ /^[^\s]+\s+from\s+.+/
557: expected = "'from <expr>'"
558: else
559: expected = "'to <expr>' or 'through <expr>'"
560: end
561: raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.")
562: end
563: raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
564: if var.slice!(0) == !!
565: offset = line.offset + line.text.index("!" + var) + 1
566: Script.var_warning(var, @line, offset, @options[:filename])
567: end
568:
569: parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
570: parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
571: Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
572: end
# File lib/sass/engine.rb, line 591
591: def parse_import(line, value)
592: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
593: :line => @line + 1) unless line.children.empty?
594:
595: scanner = StringScanner.new(value)
596: values = []
597:
598: loop do
599: unless node = parse_import_arg(scanner)
600: raise SyntaxError.new("Invalid @import: expected file to import, was #{scanner.rest.inspect}",
601: :line => @line)
602: end
603: values << node
604: break unless scanner.scan(/,\s*/)
605: end
606:
607: return values
608: end
# File lib/sass/engine.rb, line 610
610: def parse_import_arg(scanner)
611: return if scanner.eos?
612: unless (str = scanner.scan(Sass::SCSS::RX::STRING)) ||
613: (uri = scanner.scan(Sass::SCSS::RX::URI))
614: return Tree::ImportNode.new(scanner.scan(/[^,]+/))
615: end
616:
617: val = scanner[1] || scanner[2]
618: scanner.scan(/\s*/)
619: if media = scanner.scan(/[^,].*/)
620: Tree::DirectiveNode.new("@import #{str || uri} #{media}")
621: elsif uri
622: Tree::DirectiveNode.new("@import #{uri}")
623: elsif val =~ /^http:\/\//
624: Tree::DirectiveNode.new("@import url(#{val})")
625: else
626: Tree::ImportNode.new(val)
627: end
628: end
# File lib/sass/engine.rb, line 681
681: def parse_interp(text, offset = 0)
682: self.class.parse_interp(text, @line, offset, :filename => @filename)
683: end
# File lib/sass/engine.rb, line 398
398: def parse_line(parent, line, root)
399: case line.text[0]
400: when PROPERTY_CHAR
401: if line.text[1] == PROPERTY_CHAR ||
402: (@options[:property_syntax] == :new &&
403: line.text =~ PROPERTY_OLD && $3.empty?)
404: # Support CSS3-style pseudo-elements,
405: # which begin with ::,
406: # as well as pseudo-classes
407: # if we're using the new property syntax
408: Tree::RuleNode.new(parse_interp(line.text))
409: else
410: name, eq, value = line.text.scan(PROPERTY_OLD)[0]
411: raise SyntaxError.new("Invalid property: \"#{line.text}\".",
412: :line => @line) if name.nil? || value.nil?
413: parse_property(name, parse_interp(name), eq, value, :old, line)
414: end
415: when !!, $$
416: parse_variable(line)
417: when COMMENT_CHAR
418: parse_comment(line.text)
419: when DIRECTIVE_CHAR
420: parse_directive(parent, line, root)
421: when ESCAPE_CHAR
422: Tree::RuleNode.new(parse_interp(line.text[1..1]))
423: when MIXIN_DEFINITION_CHAR
424: parse_mixin_definition(line)
425: when MIXIN_INCLUDE_CHAR
426: if line.text[1].nil? || line.text[1] == \s\
427: Tree::RuleNode.new(parse_interp(line.text))
428: else
429: parse_mixin_include(line, root)
430: end
431: else
432: parse_property_or_rule(line)
433: end
434: end
# File lib/sass/engine.rb, line 631
631: def parse_mixin_definition(line)
632: name, arg_string = line.text.scan(MIXIN_DEF_RE).first
633: raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
634:
635: offset = line.offset + line.text.size - arg_string.size
636: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
637: parse_mixin_definition_arglist
638: default_arg_found = false
639: Tree::MixinDefNode.new(name, args)
640: end
# File lib/sass/engine.rb, line 643
643: def parse_mixin_include(line, root)
644: name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first
645: raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
646:
647: offset = line.offset + line.text.size - arg_string.size
648: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
649: parse_mixin_include_arglist
650: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.",
651: :line => @line + 1) unless line.children.empty?
652: Tree::MixinNode.new(name, args)
653: end
# File lib/sass/engine.rb, line 458
458: def parse_property(name, parsed_name, eq, value, prop, line)
459: if value.strip.empty?
460: expr = Sass::Script::String.new("")
461: else
462: expr = parse_script(value, :offset => line.offset + line.text.index(value))
463:
464: if eq.strip[0] == SCRIPT_CHAR
465: expr.context = :equals
466: Script.equals_warning("properties", name,
467: Sass::Tree::PropNode.val_to_sass(expr, @options), false,
468: @line, line.offset + 1, @options[:filename])
469: end
470: end
471: Tree::PropNode.new(parse_interp(name), expr, prop)
472: end
# File lib/sass/engine.rb, line 436
436: def parse_property_or_rule(line)
437: scanner = StringScanner.new(line.text)
438: hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
439: parser = Sass::SCSS::SassParser.new(scanner, @line)
440:
441: unless res = parser.parse_interp_ident
442: return Tree::RuleNode.new(parse_interp(line.text))
443: end
444: res.unshift(hack_char) if hack_char
445: if comment = scanner.scan(Sass::SCSS::RX::COMMENT)
446: res << comment
447: end
448:
449: name = line.text[0...scanner.pos]
450: if scanner.scan(/\s*([:=])(?:\s|$)/)
451: parse_property(name, res, scanner[1], scanner.rest, :new, line)
452: else
453: res.pop if comment
454: Tree::RuleNode.new(res + parse_interp(scanner.rest))
455: end
456: end
# File lib/sass/engine.rb, line 655
655: def parse_script(script, options = {})
656: line = options[:line] || @line
657: offset = options[:offset] || 0
658: Script.parse(script, line, offset, @options)
659: end
# File lib/sass/engine.rb, line 474
474: def parse_variable(line)
475: name, op, value, default = line.text.scan(Script::MATCH)[0]
476: guarded = op =~ /^\|\|/
477: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.",
478: :line => @line + 1) unless line.children.empty?
479: raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
480: :line => @line) unless name && value
481: Script.var_warning(name, @line, line.offset + 1, @options[:filename]) if line.text[0] == !!
482:
483: expr = parse_script(value, :offset => line.offset + line.text.index(value))
484: if op =~ /=$/
485: expr.context = :equals
486: type = guarded ? "variable defaults" : "variables"
487: Script.equals_warning(type, "$#{name}", expr.to_sass,
488: guarded, @line, line.offset + 1, @options[:filename])
489: end
490:
491: Tree::VariableNode.new(name, expr, default || guarded)
492: end
# File lib/sass/engine.rb, line 227
227: def tabulate(string)
228: tab_str = nil
229: comment_tab_str = nil
230: first = true
231: lines = []
232: string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^.*?$/).each_with_index do |line, index|
233: index += (@options[:line] || 1)
234: if line.strip.empty?
235: lines.last.text << "\n" if lines.last && lines.last.comment?
236: next
237: end
238:
239: line_tab_str = line[/^\s*/]
240: unless line_tab_str.empty?
241: if tab_str.nil?
242: comment_tab_str ||= line_tab_str
243: next if try_comment(line, lines.last, "", comment_tab_str, index)
244: comment_tab_str = nil
245: end
246:
247: tab_str ||= line_tab_str
248:
249: raise SyntaxError.new("Indenting at the beginning of the document is illegal.",
250: :line => index) if first
251:
252: raise SyntaxError.new("Indentation can't use both tabs and spaces.",
253: :line => index) if tab_str.include?(\s\) && tab_str.include?(\t\)
254: end
255: first &&= !tab_str.nil?
256: if tab_str.nil?
257: lines << Line.new(line.strip, 0, index, 0, @options[:filename], [])
258: next
259: end
260:
261: comment_tab_str ||= line_tab_str
262: if try_comment(line, lines.last, tab_str * lines.last.tabs, comment_tab_str, index)
263: next
264: else
265: comment_tab_str = nil
266: end
267:
268: line_tabs = line_tab_str.scan(tab_str).size
269: if tab_str * line_tabs != line_tab_str
270: message = Inconsistent indentation: #{Haml::Shared.human_indentation line_tab_str, true} used for indentation,but the rest of the document was indented using #{Haml::Shared.human_indentation tab_str}..strip.gsub("\n", ' ')
271: raise SyntaxError.new(message, :line => index)
272: end
273:
274: lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
275: end
276: lines
277: end
# File lib/sass/engine.rb, line 299
299: def tree(arr, i = 0)
300: return [], i if arr[i].nil?
301:
302: base = arr[i].tabs
303: nodes = []
304: while (line = arr[i]) && line.tabs >= base
305: if line.tabs > base
306: raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.",
307: :line => line.index) if line.tabs > base + 1
308:
309: nodes.last.children, i = tree(arr, i)
310: else
311: nodes << line
312: i += 1
313: end
314: end
315: return nodes, i
316: end
# File lib/sass/engine.rb, line 282
282: def try_comment(line, last, tab_str, comment_tab_str, index)
283: return unless last && last.comment?
284: # Nested comment stuff must be at least one whitespace char deeper
285: # than the normal indentation
286: return unless line =~ /^#{tab_str}\s/
287: unless line =~ /^(?:#{comment_tab_str})(.*)$/
288: raise SyntaxError.new(Inconsistent indentation:previous line was indented by #{Haml::Shared.human_indentation comment_tab_str},but this line was indented by #{Haml::Shared.human_indentation line[/^\s*/]}..strip.gsub("\n", " "), :line => index)
289: end
290:
291: last.text << "\n" << $1
292: true
293: end
# File lib/sass/engine.rb, line 381
381: def validate_and_append_child(parent, child, line, root)
382: case child
383: when Array
384: child.each {|c| validate_and_append_child(parent, c, line, root)}
385: when Tree::Node
386: parent << child
387: end
388: end
Disabled; run with --debug to generate this.
Generated with the Darkfish Rdoc Generator 1.1.6.