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.