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