| Class | Sass::Engine |
| In: |
lib/sass/engine.rb
|
| Parent: | Object |
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
| PROPERTY_CHAR | = | ?: | The character that begins a CSS property. | |
| SCRIPT_CHAR | = | ?= | The character that designates that a property should be assigned to a SassScript expression. | |
| COMMENT_CHAR | = | ?/ | The character that designates the beginning of a comment, either Sass or CSS. | |
| SASS_COMMENT_CHAR | = | ?/ | The character that follows the general COMMENT_CHAR and designates a Sass comment, which is not output as a CSS comment. | |
| CSS_COMMENT_CHAR | = | ?* | The character that follows the general COMMENT_CHAR and designates a CSS comment, which is embedded in the CSS document. | |
| DIRECTIVE_CHAR | = | ?@ | The character used to denote a compiler directive. | |
| ESCAPE_CHAR | = | ?\\ | Designates a non-parsed rule. | |
| MIXIN_DEFINITION_CHAR | = | ?= | Designates block as mixin definition rather than CSS rules to output | |
| MIXIN_INCLUDE_CHAR | = | ?+ | Includes named mixin declared using MIXIN_DEFINITION_CHAR | |
| PROPERTY_NEW_MATCHER | = | /^[^\s:"\[]+\s*[=:](\s|$)/ | The regex that matches properties of the form `name: prop`. | |
| PROPERTY_NEW | = | /^([^\s=:"]+)\s*(=|:)(?:\s+|$)(.*)/ | The regex that matches and extracts data from properties of the form `name: prop`. | |
| PROPERTY_OLD | = | /^:([^\s=:"]+)\s*(=?)(?:\s+|$)(.*)/ | The regex that matches and extracts data from properties of the form `:name prop`. | |
| DEFAULT_OPTIONS | = | { :style => :nested, :load_paths => ['.'], :cache => true, :cache_location => './.sass-cache', :syntax => :sass, }.freeze | The default options for Sass::Engine. @api public | |
| MIXIN_DEF_RE | = | /^(?:=|@mixin)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/ | ||
| MIXIN_INCLUDE_RE | = | /^(?:\+|@include)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/ |
@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 661
661: def self.parse_interp(text, line, offset, options)
662: res = []
663: rest = Haml::Shared.handle_interpolation text do |scan|
664: escapes = scan[2].size
665: res << scan.matched[0...-2 - escapes]
666: if escapes % 2 == 1
667: res << "\\" * (escapes - 1) << '#{'
668: else
669: res << "\\" * [0, escapes - 1].max
670: res << Script::Parser.new(
671: scan, line, offset + scan.pos - scan.matched_size, options).
672: parse_interpolated
673: end
674: end
675: res << rest
676: 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 337
337: def append_children(parent, children, root)
338: continued_rule = nil
339: continued_comment = nil
340: children.each do |line|
341: child = build_tree(parent, line, root)
342:
343: if child.is_a?(Tree::RuleNode) && child.continued?
344: raise SyntaxError.new("Rules can't end in commas.",
345: :line => child.line) unless child.children.empty?
346: if continued_rule
347: continued_rule.add_rules child
348: else
349: continued_rule = child
350: end
351: next
352: end
353:
354: if continued_rule
355: raise SyntaxError.new("Rules can't end in commas.",
356: :line => continued_rule.line) unless child.is_a?(Tree::RuleNode)
357: continued_rule.add_rules child
358: continued_rule.children = child.children
359: continued_rule, child = nil, continued_rule
360: end
361:
362: if child.is_a?(Tree::CommentNode) && child.silent
363: if continued_comment &&
364: child.line == continued_comment.line +
365: continued_comment.value.count("\n") + 1
366: continued_comment.value << "\n" << child.value
367: next
368: end
369:
370: continued_comment = child
371: end
372:
373: check_for_no_children(child)
374: validate_and_append_child(parent, child, line, root)
375: end
376:
377: raise SyntaxError.new("Rules can't end in commas.",
378: :line => continued_rule.line) if continued_rule
379:
380: parent
381: end
# File lib/sass/engine.rb, line 320
320: def build_tree(parent, line, root = false)
321: @line = line.index
322: node_or_nodes = parse_line(parent, line, root)
323:
324: Array(node_or_nodes).each do |node|
325: # Node is a symbol if it's non-outputting, like a variable assignment
326: next unless node.is_a? Tree::Node
327:
328: node.line = line.index
329: node.filename = line.filename
330:
331: append_children(node, line.children, false)
332: end
333:
334: node_or_nodes
335: 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 392
392: def check_for_no_children(node)
393: return unless node.is_a?(Tree::RuleNode) && node.children.empty?
394: Haml::Util.haml_warn("WARNING on line \#{node.line}\#{\" of \#{node.filename}\" if node.filename}:\nThis selector doesn't have any properties and will not be rendered.\n".strip)
395: end
# File lib/sass/engine.rb, line 634
634: def format_comment_text(text, silent)
635: content = text.split("\n")
636:
637: if content.first && content.first.strip.empty?
638: removed_first = true
639: content.shift
640: end
641:
642: return silent ? "//" : "/* */" if content.empty?
643: content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l}
644: content.first.gsub!(/^ /, '') unless removed_first
645: content.last.gsub!(%r{ ?\*/ *$}, '')
646: if silent
647: "//" + content.join("\n//")
648: else
649: "/*" + content.join("\n *") + " */"
650: end
651: end
# File lib/sass/engine.rb, line 497
497: def parse_comment(line)
498: if line[1] == CSS_COMMENT_CHAR || line[1] == SASS_COMMENT_CHAR
499: silent = line[1] == SASS_COMMENT_CHAR
500: Tree::CommentNode.new(
501: format_comment_text(line[2..-1], silent),
502: silent)
503: else
504: Tree::RuleNode.new(parse_interp(line))
505: end
506: end
# File lib/sass/engine.rb, line 508
508: def parse_directive(parent, line, root)
509: directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
510: offset = directive.size + whitespace.size + 1 if whitespace
511:
512: # If value begins with url( or ",
513: # it's a CSS @import rule and we don't want to touch it.
514: if directive == "import"
515: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
516: :line => @line + 1) unless line.children.empty?
517: if (match = value.match(Sass::SCSS::RX::STRING) || value.match(Sass::SCSS::RX::URI)) &&
518: !match.post_match.strip.empty? && match.post_match.strip[0] != ?,
519: return Tree::DirectiveNode.new("@import #{value}")
520: end
521: value.split(/,\s*/).map do |f|
522: f = $1 || $2 || $3 if f =~ Sass::SCSS::RX::STRING || f =~ Sass::SCSS::RX::URI
523: Tree::ImportNode.new(f)
524: end
525: elsif directive == "mixin"
526: parse_mixin_definition(line)
527: elsif directive == "include"
528: parse_mixin_include(line, root)
529: elsif directive == "for"
530: parse_for(line, root, value)
531: elsif directive == "else"
532: parse_else(parent, line, value)
533: elsif directive == "while"
534: raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
535: Tree::WhileNode.new(parse_script(value, :offset => offset))
536: elsif directive == "if"
537: raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
538: Tree::IfNode.new(parse_script(value, :offset => offset))
539: elsif directive == "debug"
540: raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
541: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
542: :line => @line + 1) unless line.children.empty?
543: offset = line.offset + line.text.index(value).to_i
544: Tree::DebugNode.new(parse_script(value, :offset => offset))
545: elsif directive == "extend"
546: raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
547: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
548: :line => @line + 1) unless line.children.empty?
549: offset = line.offset + line.text.index(value).to_i
550: Tree::ExtendNode.new(parse_interp(value, offset))
551: elsif directive == "warn"
552: raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
553: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
554: :line => @line + 1) unless line.children.empty?
555: offset = line.offset + line.text.index(value).to_i
556: Tree::WarnNode.new(parse_script(value, :offset => offset))
557: else
558: Tree::DirectiveNode.new(line.text)
559: end
560: end
# File lib/sass/engine.rb, line 586
586: def parse_else(parent, line, text)
587: previous = parent.children.last
588: raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
589:
590: if text
591: if text !~ /^if\s+(.+)/
592: raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.")
593: end
594: expr = parse_script($1, :offset => line.offset + line.text.index($1))
595: end
596:
597: node = Tree::IfNode.new(expr)
598: append_children(node, line.children, false)
599: previous.add_else node
600: nil
601: end
# File lib/sass/engine.rb, line 562
562: def parse_for(line, root, text)
563: var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
564:
565: if var.nil? # scan failed, try to figure out why for error message
566: if text !~ /^[^\s]+/
567: expected = "variable name"
568: elsif text !~ /^[^\s]+\s+from\s+.+/
569: expected = "'from <expr>'"
570: else
571: expected = "'to <expr>' or 'through <expr>'"
572: end
573: raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.")
574: end
575: raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
576: if var.slice!(0) == ?!
577: offset = line.offset + line.text.index("!" + var) + 1
578: Script.var_warning(var, @line, offset, @options[:filename])
579: end
580:
581: parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
582: parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
583: Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
584: end
# File lib/sass/engine.rb, line 653
653: def parse_interp(text, offset = 0)
654: self.class.parse_interp(text, @line, offset, :filename => @filename)
655: end
# File lib/sass/engine.rb, line 401
401: def parse_line(parent, line, root)
402: case line.text[0]
403: when PROPERTY_CHAR
404: if line.text[1] == PROPERTY_CHAR ||
405: (@options[:property_syntax] == :new &&
406: line.text =~ PROPERTY_OLD && $3.empty?)
407: # Support CSS3-style pseudo-elements,
408: # which begin with ::,
409: # as well as pseudo-classes
410: # if we're using the new property syntax
411: Tree::RuleNode.new(parse_interp(line.text))
412: else
413: name, eq, value = line.text.scan(PROPERTY_OLD)[0]
414: raise SyntaxError.new("Invalid property: \"#{line.text}\".",
415: :line => @line) if name.nil? || value.nil?
416: parse_property(name, parse_interp(name), eq, value, :old, line)
417: end
418: when ?!, ?$
419: parse_variable(line)
420: when COMMENT_CHAR
421: parse_comment(line.text)
422: when DIRECTIVE_CHAR
423: parse_directive(parent, line, root)
424: when ESCAPE_CHAR
425: Tree::RuleNode.new(parse_interp(line.text[1..-1]))
426: when MIXIN_DEFINITION_CHAR
427: parse_mixin_definition(line)
428: when MIXIN_INCLUDE_CHAR
429: if line.text[1].nil? || line.text[1] == ?\s
430: Tree::RuleNode.new(parse_interp(line.text))
431: else
432: parse_mixin_include(line, root)
433: end
434: else
435: parse_property_or_rule(line)
436: end
437: end
# File lib/sass/engine.rb, line 604
604: def parse_mixin_definition(line)
605: name, arg_string = line.text.scan(MIXIN_DEF_RE).first
606: raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
607:
608: offset = line.offset + line.text.size - arg_string.size
609: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
610: parse_mixin_definition_arglist
611: default_arg_found = false
612: Tree::MixinDefNode.new(name, args)
613: end
# File lib/sass/engine.rb, line 616
616: def parse_mixin_include(line, root)
617: name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first
618: raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
619:
620: offset = line.offset + line.text.size - arg_string.size
621: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
622: parse_mixin_include_arglist
623: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.",
624: :line => @line + 1) unless line.children.empty?
625: Tree::MixinNode.new(name, args)
626: end
# File lib/sass/engine.rb, line 461
461: def parse_property(name, parsed_name, eq, value, prop, line)
462: if value.strip.empty?
463: expr = Sass::Script::String.new("")
464: else
465: expr = parse_script(value, :offset => line.offset + line.text.index(value))
466:
467: if eq.strip[0] == SCRIPT_CHAR
468: expr.context = :equals
469: Script.equals_warning("properties", name,
470: Sass::Tree::PropNode.val_to_sass(expr, @options), false,
471: @line, line.offset + 1, @options[:filename])
472: end
473: end
474: Tree::PropNode.new(parse_interp(name), expr, prop)
475: end
# File lib/sass/engine.rb, line 439
439: def parse_property_or_rule(line)
440: scanner = StringScanner.new(line.text)
441: hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
442: parser = Sass::SCSS::SassParser.new(scanner, @line)
443:
444: unless res = parser.parse_interp_ident
445: return Tree::RuleNode.new(parse_interp(line.text))
446: end
447: res.unshift(hack_char) if hack_char
448: if comment = scanner.scan(Sass::SCSS::RX::COMMENT)
449: res << comment
450: end
451:
452: name = line.text[0...scanner.pos]
453: if scanner.scan(/\s*([:=])(?:\s|$)/)
454: parse_property(name, res, scanner[1], scanner.rest, :new, line)
455: else
456: res.pop if comment
457: Tree::RuleNode.new(res + parse_interp(scanner.rest))
458: end
459: end
# File lib/sass/engine.rb, line 628
628: def parse_script(script, options = {})
629: line = options[:line] || @line
630: offset = options[:offset] || 0
631: Script.parse(script, line, offset, @options)
632: end
# File lib/sass/engine.rb, line 477
477: def parse_variable(line)
478: name, op, value, default = line.text.scan(Script::MATCH)[0]
479: guarded = op =~ /^\|\|/
480: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.",
481: :line => @line + 1) unless line.children.empty?
482: raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
483: :line => @line) unless name && value
484: Script.var_warning(name, @line, line.offset + 1, @options[:filename]) if line.text[0] == ?!
485:
486: expr = parse_script(value, :offset => line.offset + line.text.index(value))
487: if op =~ /=$/
488: expr.context = :equals
489: type = guarded ? "variable defaults" : "variables"
490: Script.equals_warning(type, "$#{name}", expr.to_sass,
491: guarded, @line, line.offset + 1, @options[:filename])
492: end
493:
494: Tree::VariableNode.new(name, expr, default || guarded)
495: 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,\nbut the rest of the document was indented using \#{Haml::Shared.human_indentation tab_str}.\n".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 301
301: def tree(arr, i = 0)
302: return [], i if arr[i].nil?
303:
304: base = arr[i].tabs
305: nodes = []
306: while (line = arr[i]) && line.tabs >= base
307: if line.tabs > base
308: raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.",
309: :line => line.index) if line.tabs > base + 1
310:
311: nodes.last.children, i = tree(arr, i)
312: else
313: nodes << line
314: i += 1
315: end
316: end
317: return nodes, i
318: end
# File lib/sass/engine.rb, line 283
283: def try_comment(line, last, tab_str, comment_tab_str, index)
284: return unless last && last.comment?
285: # Nested comment stuff must be at least one whitespace char deeper
286: # than the normal indentation
287: return unless line =~ /^#{tab_str}\s/
288: unless line =~ /^(?:#{comment_tab_str})(.*)$/
289: raise SyntaxError.new("Inconsistent indentation:\nprevious line was indented by \#{Haml::Shared.human_indentation comment_tab_str},\nbut this line was indented by \#{Haml::Shared.human_indentation line[/^\\s*/]}.\n".strip.gsub("\n", " "), :line => index)
290: end
291:
292: last.text << "\n" << $1
293: true
294: end