| 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 691
691: def self.parse_interp(text, line, offset, options)
692: res = []
693: rest = Haml::Shared.handle_interpolation text do |scan|
694: escapes = scan[2].size
695: res << scan.matched[0...-2 - escapes]
696: if escapes % 2 == 1
697: res << "\\" * (escapes - 1) << '#{'
698: else
699: res << "\\" * [0, escapes - 1].max
700: res << Script::Parser.new(
701: scan, line, offset + scan.pos - scan.matched_size, options).
702: parse_interpolated
703: end
704: end
705: res << rest
706: 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 664
664: def format_comment_text(text, silent)
665: content = text.split("\n")
666:
667: if content.first && content.first.strip.empty?
668: removed_first = true
669: content.shift
670: end
671:
672: return silent ? "//" : "/* */" if content.empty?
673: content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l}
674: content.first.gsub!(/^ /, '') unless removed_first
675: content.last.gsub!(%r{ ?\*/ *$}, '')
676: if silent
677: "//" + content.join("\n//")
678: else
679: "/*" + content.join("\n *") + " */"
680: end
681: 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: parse_import(line, value)
516: elsif directive == "mixin"
517: parse_mixin_definition(line)
518: elsif directive == "include"
519: parse_mixin_include(line, root)
520: elsif directive == "for"
521: parse_for(line, root, value)
522: elsif directive == "else"
523: parse_else(parent, line, value)
524: elsif directive == "while"
525: raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
526: Tree::WhileNode.new(parse_script(value, :offset => offset))
527: elsif directive == "if"
528: raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
529: Tree::IfNode.new(parse_script(value, :offset => offset))
530: elsif directive == "debug"
531: raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
532: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
533: :line => @line + 1) unless line.children.empty?
534: offset = line.offset + line.text.index(value).to_i
535: Tree::DebugNode.new(parse_script(value, :offset => offset))
536: elsif directive == "extend"
537: raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
538: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
539: :line => @line + 1) unless line.children.empty?
540: offset = line.offset + line.text.index(value).to_i
541: Tree::ExtendNode.new(parse_interp(value, offset))
542: elsif directive == "warn"
543: raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
544: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
545: :line => @line + 1) unless line.children.empty?
546: offset = line.offset + line.text.index(value).to_i
547: Tree::WarnNode.new(parse_script(value, :offset => offset))
548: else
549: Tree::DirectiveNode.new(line.text)
550: end
551: end
# File lib/sass/engine.rb, line 577
577: def parse_else(parent, line, text)
578: previous = parent.children.last
579: raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
580:
581: if text
582: if text !~ /^if\s+(.+)/
583: raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.")
584: end
585: expr = parse_script($1, :offset => line.offset + line.text.index($1))
586: end
587:
588: node = Tree::IfNode.new(expr)
589: append_children(node, line.children, false)
590: previous.add_else node
591: nil
592: end
# File lib/sass/engine.rb, line 553
553: def parse_for(line, root, text)
554: var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
555:
556: if var.nil? # scan failed, try to figure out why for error message
557: if text !~ /^[^\s]+/
558: expected = "variable name"
559: elsif text !~ /^[^\s]+\s+from\s+.+/
560: expected = "'from <expr>'"
561: else
562: expected = "'to <expr>' or 'through <expr>'"
563: end
564: raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.")
565: end
566: raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
567: if var.slice!(0) == ?!
568: offset = line.offset + line.text.index("!" + var) + 1
569: Script.var_warning(var, @line, offset, @options[:filename])
570: end
571:
572: parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
573: parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
574: Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
575: end
# File lib/sass/engine.rb, line 594
594: def parse_import(line, value)
595: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
596: :line => @line + 1) unless line.children.empty?
597:
598: scanner = StringScanner.new(value)
599: values = []
600:
601: loop do
602: unless node = parse_import_arg(scanner)
603: raise SyntaxError.new("Invalid @import: expected file to import, was #{scanner.rest.inspect}",
604: :line => @line)
605: end
606: values << node
607: break unless scanner.scan(/,\s*/)
608: end
609:
610: return values
611: end
# File lib/sass/engine.rb, line 613
613: def parse_import_arg(scanner)
614: return if scanner.eos?
615: unless (str = scanner.scan(Sass::SCSS::RX::STRING)) ||
616: (uri = scanner.scan(Sass::SCSS::RX::URI))
617: return Tree::ImportNode.new(scanner.scan(/[^,]+/))
618: end
619:
620: val = scanner[1] || scanner[2]
621: scanner.scan(/\s*/)
622: if media = scanner.scan(/[^,].*/)
623: Tree::DirectiveNode.new("@import #{str || uri} #{media}")
624: elsif uri
625: Tree::DirectiveNode.new("@import #{uri}")
626: elsif val =~ /^http:\/\//
627: Tree::DirectiveNode.new("@import url(#{val})")
628: else
629: Tree::ImportNode.new(val)
630: end
631: end
# File lib/sass/engine.rb, line 683
683: def parse_interp(text, offset = 0)
684: self.class.parse_interp(text, @line, offset, :filename => @filename)
685: 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 634
634: def parse_mixin_definition(line)
635: name, arg_string = line.text.scan(MIXIN_DEF_RE).first
636: raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
637:
638: offset = line.offset + line.text.size - arg_string.size
639: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
640: parse_mixin_definition_arglist
641: default_arg_found = false
642: Tree::MixinDefNode.new(name, args)
643: end
# File lib/sass/engine.rb, line 646
646: def parse_mixin_include(line, root)
647: name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first
648: raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
649:
650: offset = line.offset + line.text.size - arg_string.size
651: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
652: parse_mixin_include_arglist
653: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.",
654: :line => @line + 1) unless line.children.empty?
655: Tree::MixinNode.new(name, args)
656: 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 658
658: def parse_script(script, options = {})
659: line = options[:line] || @line
660: offset = options[:offset] || 0
661: Script.parse(script, line, offset, @options)
662: 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