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