| 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 144
144: def initialize(template, options={})
145: @options = DEFAULT_OPTIONS.merge(options.reject {|k, v| v.nil?})
146: @template = template
147:
148: # Support both, because the docs said one and the other actually worked
149: # for quite a long time.
150: @options[:line_comments] ||= @options[:line_numbers]
151:
152: # Backwards compatibility
153: @options[:property_syntax] ||= @options[:attribute_syntax]
154: case @options[:property_syntax]
155: when :alternate; @options[:property_syntax] = :new
156: when :normal; @options[:property_syntax] = :old
157: end
158: 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 706
706: def self.parse_interp(text, line, offset, options)
707: res = []
708: rest = Haml::Shared.handle_interpolation text do |scan|
709: escapes = scan[2].size
710: res << scan.matched[0...-2 - escapes]
711: if escapes % 2 == 1
712: res << "\\" * (escapes - 1) << '#{'
713: else
714: res << "\\" * [0, escapes - 1].max
715: res << Script::Parser.new(
716: scan, line, offset + scan.pos - scan.matched_size, options).
717: parse_interpolated
718: end
719: end
720: res << rest
721: 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 167
167: def render
168: return _render unless @options[:quiet]
169: Haml::Util.silence_haml_warnings {_render}
170: 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 189
189: def source_encoding
190: check_encoding!
191: @original_encoding
192: 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 177
177: def to_tree
178: return _to_tree unless @options[:quiet]
179: Haml::Util.silence_haml_warnings {_to_tree}
180: end
# File lib/sass/engine.rb, line 196
196: def _render
197: rendered = _to_tree.render
198: return rendered if ruby1_8?
199: begin
200: # Try to convert the result to the original encoding,
201: # but if that doesn't work fall back on UTF-8
202: rendered = rendered.encode(source_encoding)
203: rescue EncodingError
204: end
205: rendered.gsub(Regexp.new('\A@charset "(.*?)"'.encode(source_encoding)),
206: "@charset \"#{source_encoding.name}\"".encode(source_encoding))
207: end
# File lib/sass/engine.rb, line 209
209: def _to_tree
210: check_encoding!
211:
212: if @options[:syntax] == :scss
213: root = Sass::SCSS::Parser.new(@template).parse
214: else
215: root = Tree::RootNode.new(@template)
216: append_children(root, tree(tabulate(@template)).first, true)
217: end
218:
219: root.options = @options
220: root
221: rescue SyntaxError => e
222: e.modify_backtrace(:filename => @options[:filename], :line => @line)
223: e.sass_template = @template
224: raise e
225: end
# File lib/sass/engine.rb, line 345
345: def append_children(parent, children, root)
346: continued_rule = nil
347: continued_comment = nil
348: children.each do |line|
349: child = build_tree(parent, line, root)
350:
351: if child.is_a?(Tree::RuleNode) && child.continued?
352: raise SyntaxError.new("Rules can't end in commas.",
353: :line => child.line) unless child.children.empty?
354: if continued_rule
355: continued_rule.add_rules child
356: else
357: continued_rule = child
358: end
359: next
360: end
361:
362: if continued_rule
363: raise SyntaxError.new("Rules can't end in commas.",
364: :line => continued_rule.line) unless child.is_a?(Tree::RuleNode)
365: continued_rule.add_rules child
366: continued_rule.children = child.children
367: continued_rule, child = nil, continued_rule
368: end
369:
370: if child.is_a?(Tree::CommentNode) && child.silent
371: if continued_comment &&
372: child.line == continued_comment.line +
373: continued_comment.value.count("\n") + 1
374: continued_comment.value << "\n" << child.value
375: next
376: end
377:
378: continued_comment = child
379: end
380:
381: check_for_no_children(child)
382: validate_and_append_child(parent, child, line, root)
383: end
384:
385: raise SyntaxError.new("Rules can't end in commas.",
386: :line => continued_rule.line) if continued_rule
387:
388: parent
389: end
# File lib/sass/engine.rb, line 328
328: def build_tree(parent, line, root = false)
329: @line = line.index
330: node_or_nodes = parse_line(parent, line, root)
331:
332: Array(node_or_nodes).each do |node|
333: # Node is a symbol if it's non-outputting, like a variable assignment
334: next unless node.is_a? Tree::Node
335:
336: node.line = line.index
337: node.filename = line.filename
338:
339: append_children(node, line.children, false)
340: end
341:
342: node_or_nodes
343: end
# File lib/sass/engine.rb, line 227
227: def check_encoding!
228: return if @checked_encoding
229: @checked_encoding = true
230: @template, @original_encoding = check_sass_encoding(@template) do |msg, line|
231: raise Sass::SyntaxError.new(msg, :line => line)
232: end
233: end
# File lib/sass/engine.rb, line 400
400: def check_for_no_children(node)
401: return unless node.is_a?(Tree::RuleNode) && node.children.empty?
402: 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)
403: end
# File lib/sass/engine.rb, line 678
678: def format_comment_text(text, silent)
679: content = text.split("\n")
680:
681: if content.first && content.first.strip.empty?
682: removed_first = true
683: content.shift
684: end
685:
686: return silent ? "//" : "/* */" if content.empty?
687: content.last.gsub!(%r{ ?\*/ *$}, '')
688: content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l}
689: content.first.gsub!(/^ /, '') unless removed_first
690: if silent
691: "//" + content.join("\n//")
692: else
693: # The #gsub fixes the case of a trailing */
694: "/*" + content.join("\n *").gsub(/ \*\Z/, '') + " */"
695: end
696: end
# File lib/sass/engine.rb, line 505
505: def parse_comment(line)
506: if line[1] == CSS_COMMENT_CHAR || line[1] == SASS_COMMENT_CHAR
507: silent = line[1] == SASS_COMMENT_CHAR
508: Tree::CommentNode.new(
509: format_comment_text(line[2..-1], silent),
510: silent)
511: else
512: Tree::RuleNode.new(parse_interp(line))
513: end
514: end
# File lib/sass/engine.rb, line 516
516: def parse_directive(parent, line, root)
517: directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
518: offset = directive.size + whitespace.size + 1 if whitespace
519:
520: # If value begins with url( or ",
521: # it's a CSS @import rule and we don't want to touch it.
522: if directive == "import"
523: parse_import(line, value)
524: elsif directive == "mixin"
525: parse_mixin_definition(line)
526: elsif directive == "include"
527: parse_mixin_include(line, root)
528: elsif directive == "for"
529: parse_for(line, root, value)
530: elsif directive == "else"
531: parse_else(parent, line, value)
532: elsif directive == "while"
533: raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
534: Tree::WhileNode.new(parse_script(value, :offset => offset))
535: elsif directive == "if"
536: raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
537: Tree::IfNode.new(parse_script(value, :offset => offset))
538: elsif directive == "debug"
539: raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
540: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
541: :line => @line + 1) unless line.children.empty?
542: offset = line.offset + line.text.index(value).to_i
543: Tree::DebugNode.new(parse_script(value, :offset => offset))
544: elsif directive == "extend"
545: raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
546: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
547: :line => @line + 1) unless line.children.empty?
548: offset = line.offset + line.text.index(value).to_i
549: Tree::ExtendNode.new(parse_interp(value, offset))
550: elsif directive == "warn"
551: raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
552: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
553: :line => @line + 1) unless line.children.empty?
554: offset = line.offset + line.text.index(value).to_i
555: Tree::WarnNode.new(parse_script(value, :offset => offset))
556: elsif directive == "charset"
557: name = value && value[/\A(["'])(.*)\1\Z/, 2] #"
558: raise SyntaxError.new("Invalid charset directive '@charset': expected string.") unless name
559: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath charset directives.",
560: :line => @line + 1) unless line.children.empty?
561: Tree::CharsetNode.new(name)
562: else
563: Tree::DirectiveNode.new(line.text)
564: end
565: end
# File lib/sass/engine.rb, line 591
591: def parse_else(parent, line, text)
592: previous = parent.children.last
593: raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
594:
595: if text
596: if text !~ /^if\s+(.+)/
597: raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.")
598: end
599: expr = parse_script($1, :offset => line.offset + line.text.index($1))
600: end
601:
602: node = Tree::IfNode.new(expr)
603: append_children(node, line.children, false)
604: previous.add_else node
605: nil
606: end
# File lib/sass/engine.rb, line 567
567: def parse_for(line, root, text)
568: var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
569:
570: if var.nil? # scan failed, try to figure out why for error message
571: if text !~ /^[^\s]+/
572: expected = "variable name"
573: elsif text !~ /^[^\s]+\s+from\s+.+/
574: expected = "'from <expr>'"
575: else
576: expected = "'to <expr>' or 'through <expr>'"
577: end
578: raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.")
579: end
580: raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
581: if var.slice!(0) == ?!
582: offset = line.offset + line.text.index("!" + var) + 1
583: Script.var_warning(var, @line, offset, @options[:filename])
584: end
585:
586: parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
587: parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
588: Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
589: end
# File lib/sass/engine.rb, line 608
608: def parse_import(line, value)
609: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
610: :line => @line + 1) unless line.children.empty?
611:
612: scanner = StringScanner.new(value)
613: values = []
614:
615: loop do
616: unless node = parse_import_arg(scanner)
617: raise SyntaxError.new("Invalid @import: expected file to import, was #{scanner.rest.inspect}",
618: :line => @line)
619: end
620: values << node
621: break unless scanner.scan(/,\s*/)
622: end
623:
624: return values
625: end
# File lib/sass/engine.rb, line 627
627: def parse_import_arg(scanner)
628: return if scanner.eos?
629: unless (str = scanner.scan(Sass::SCSS::RX::STRING)) ||
630: (uri = scanner.scan(Sass::SCSS::RX::URI))
631: return Tree::ImportNode.new(scanner.scan(/[^,]+/))
632: end
633:
634: val = scanner[1] || scanner[2]
635: scanner.scan(/\s*/)
636: if media = scanner.scan(/[^,].*/)
637: Tree::DirectiveNode.new("@import #{str || uri} #{media}")
638: elsif uri
639: Tree::DirectiveNode.new("@import #{uri}")
640: elsif val =~ /^http:\/\//
641: Tree::DirectiveNode.new("@import url(#{val})")
642: else
643: Tree::ImportNode.new(val)
644: end
645: end
# File lib/sass/engine.rb, line 698
698: def parse_interp(text, offset = 0)
699: self.class.parse_interp(text, @line, offset, :filename => @filename)
700: end
# File lib/sass/engine.rb, line 409
409: def parse_line(parent, line, root)
410: case line.text[0]
411: when PROPERTY_CHAR
412: if line.text[1] == PROPERTY_CHAR ||
413: (@options[:property_syntax] == :new &&
414: line.text =~ PROPERTY_OLD && $3.empty?)
415: # Support CSS3-style pseudo-elements,
416: # which begin with ::,
417: # as well as pseudo-classes
418: # if we're using the new property syntax
419: Tree::RuleNode.new(parse_interp(line.text))
420: else
421: name, eq, value = line.text.scan(PROPERTY_OLD)[0]
422: raise SyntaxError.new("Invalid property: \"#{line.text}\".",
423: :line => @line) if name.nil? || value.nil?
424: parse_property(name, parse_interp(name), eq, value, :old, line)
425: end
426: when ?!, ?$
427: parse_variable(line)
428: when COMMENT_CHAR
429: parse_comment(line.text)
430: when DIRECTIVE_CHAR
431: parse_directive(parent, line, root)
432: when ESCAPE_CHAR
433: Tree::RuleNode.new(parse_interp(line.text[1..-1]))
434: when MIXIN_DEFINITION_CHAR
435: parse_mixin_definition(line)
436: when MIXIN_INCLUDE_CHAR
437: if line.text[1].nil? || line.text[1] == ?\s
438: Tree::RuleNode.new(parse_interp(line.text))
439: else
440: parse_mixin_include(line, root)
441: end
442: else
443: parse_property_or_rule(line)
444: end
445: end
# File lib/sass/engine.rb, line 648
648: def parse_mixin_definition(line)
649: name, arg_string = line.text.scan(MIXIN_DEF_RE).first
650: raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
651:
652: offset = line.offset + line.text.size - arg_string.size
653: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
654: parse_mixin_definition_arglist
655: default_arg_found = false
656: Tree::MixinDefNode.new(name, args)
657: end
# File lib/sass/engine.rb, line 660
660: def parse_mixin_include(line, root)
661: name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first
662: raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
663:
664: offset = line.offset + line.text.size - arg_string.size
665: args = Script::Parser.new(arg_string.strip, @line, offset, @options).
666: parse_mixin_include_arglist
667: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.",
668: :line => @line + 1) unless line.children.empty?
669: Tree::MixinNode.new(name, args)
670: end
# File lib/sass/engine.rb, line 469
469: def parse_property(name, parsed_name, eq, value, prop, line)
470: if value.strip.empty?
471: expr = Sass::Script::String.new("")
472: else
473: expr = parse_script(value, :offset => line.offset + line.text.index(value))
474:
475: if eq.strip[0] == SCRIPT_CHAR
476: expr.context = :equals
477: Script.equals_warning("properties", name,
478: Sass::Tree::PropNode.val_to_sass(expr, @options), false,
479: @line, line.offset + 1, @options[:filename])
480: end
481: end
482: Tree::PropNode.new(parse_interp(name), expr, prop)
483: end
# File lib/sass/engine.rb, line 447
447: def parse_property_or_rule(line)
448: scanner = StringScanner.new(line.text)
449: hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
450: parser = Sass::SCSS::SassParser.new(scanner, @line)
451:
452: unless res = parser.parse_interp_ident
453: return Tree::RuleNode.new(parse_interp(line.text))
454: end
455: res.unshift(hack_char) if hack_char
456: if comment = scanner.scan(Sass::SCSS::RX::COMMENT)
457: res << comment
458: end
459:
460: name = line.text[0...scanner.pos]
461: if scanner.scan(/\s*([:=])(?:\s|$)/)
462: parse_property(name, res, scanner[1], scanner.rest, :new, line)
463: else
464: res.pop if comment
465: Tree::RuleNode.new(res + parse_interp(scanner.rest))
466: end
467: end
# File lib/sass/engine.rb, line 672
672: def parse_script(script, options = {})
673: line = options[:line] || @line
674: offset = options[:offset] || 0
675: Script.parse(script, line, offset, @options)
676: end
# File lib/sass/engine.rb, line 485
485: def parse_variable(line)
486: name, op, value, default = line.text.scan(Script::MATCH)[0]
487: guarded = op =~ /^\|\|/
488: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.",
489: :line => @line + 1) unless line.children.empty?
490: raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
491: :line => @line) unless name && value
492: Script.var_warning(name, @line, line.offset + 1, @options[:filename]) if line.text[0] == ?!
493:
494: expr = parse_script(value, :offset => line.offset + line.text.index(value))
495: if op =~ /=$/
496: expr.context = :equals
497: type = guarded ? "variable defaults" : "variables"
498: Script.equals_warning(type, "$#{name}", expr.to_sass,
499: guarded, @line, line.offset + 1, @options[:filename])
500: end
501:
502: Tree::VariableNode.new(name, expr, default || guarded)
503: end
# File lib/sass/engine.rb, line 235
235: def tabulate(string)
236: tab_str = nil
237: comment_tab_str = nil
238: first = true
239: lines = []
240: string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^[^\n]*?$/).each_with_index do |line, index|
241: index += (@options[:line] || 1)
242: if line.strip.empty?
243: lines.last.text << "\n" if lines.last && lines.last.comment?
244: next
245: end
246:
247: line_tab_str = line[/^\s*/]
248: unless line_tab_str.empty?
249: if tab_str.nil?
250: comment_tab_str ||= line_tab_str
251: next if try_comment(line, lines.last, "", comment_tab_str, index)
252: comment_tab_str = nil
253: end
254:
255: tab_str ||= line_tab_str
256:
257: raise SyntaxError.new("Indenting at the beginning of the document is illegal.",
258: :line => index) if first
259:
260: raise SyntaxError.new("Indentation can't use both tabs and spaces.",
261: :line => index) if tab_str.include?(?\s) && tab_str.include?(?\t)
262: end
263: first &&= !tab_str.nil?
264: if tab_str.nil?
265: lines << Line.new(line.strip, 0, index, 0, @options[:filename], [])
266: next
267: end
268:
269: comment_tab_str ||= line_tab_str
270: if try_comment(line, lines.last, tab_str * lines.last.tabs, comment_tab_str, index)
271: next
272: else
273: comment_tab_str = nil
274: end
275:
276: line_tabs = line_tab_str.scan(tab_str).size
277: if tab_str * line_tabs != line_tab_str
278: 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", ' ')
279: raise SyntaxError.new(message, :line => index)
280: end
281:
282: lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
283: end
284: lines
285: end
# File lib/sass/engine.rb, line 309
309: def tree(arr, i = 0)
310: return [], i if arr[i].nil?
311:
312: base = arr[i].tabs
313: nodes = []
314: while (line = arr[i]) && line.tabs >= base
315: if line.tabs > base
316: raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.",
317: :line => line.index) if line.tabs > base + 1
318:
319: nodes.last.children, i = tree(arr, i)
320: else
321: nodes << line
322: i += 1
323: end
324: end
325: return nodes, i
326: end
# File lib/sass/engine.rb, line 291
291: def try_comment(line, last, tab_str, comment_tab_str, index)
292: return unless last && last.comment?
293: # Nested comment stuff must be at least one whitespace char deeper
294: # than the normal indentation
295: return unless line =~ /^#{tab_str}\s/
296: unless line =~ /^(?:#{comment_tab_str})(.*)$/
297: 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)
298: end
299:
300: last.text << "\n" << $1
301: true
302: end