| Class | Sass::Engine |
| In: |
lib/sass/engine.rb
|
| Parent: | Object |
This is the class where all the parsing and processing of the Sass template is done. It can be directly used by the user by creating a new instance and calling render to render the template. For example:
template = File.load('stylesheets/sassy.sass')
sass_engine = Sass::Engine.new(template)
output = sass_engine.render
puts output
| ATTRIBUTE_CHAR | = | ?: | The character that begins a CSS attribute. | |
| SCRIPT_CHAR | = | ?= | The character that designates that an attribute should be assigned to the result of constant arithmetic. | |
| 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 | |
| ATTRIBUTE | = | /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/ | The regex that matches and extracts data from attributes of the form :name attr. | |
| ATTRIBUTE_ALTERNATE_MATCHER | = | /^[^\s:]+\s*[=:](\s|$)/ | The regex that matches attributes of the form name: attr. | |
| ATTRIBUTE_ALTERNATE | = | /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/ | The regex that matches and extracts data from attributes of the form name: attr. |
Creates a new instace of Sass::Engine that will compile the given template string when render is called. See README.rdoc for available options.
# File lib/sass/engine.rb, line 74
74: def initialize(template, options={})
75: @options = {
76: :style => :nested,
77: :load_paths => ['.']
78: }.merge! options
79: @template = template.split(/\r\n|\r|\n/)
80: @lines = []
81: @constants = {"important" => "!important"}
82: @mixins = {}
83: end
# File lib/sass/engine.rb, line 438
438: def self.find_file_to_import(filename, load_paths)
439: was_sass = false
440: original_filename = filename
441:
442: if filename[-5..-1] == ".sass"
443: filename = filename[0...-5]
444: was_sass = true
445: elsif filename[-4..-1] == ".css"
446: return filename
447: end
448:
449: new_filename = find_full_path("#{filename}.sass", load_paths)
450:
451: return new_filename if new_filename
452: return filename + '.css' unless was_sass
453: raise SyntaxError.new("File to import not found or unreadable: #{original_filename}.", @line)
454: end
# File lib/sass/engine.rb, line 456
456: def self.find_full_path(filename, load_paths)
457: load_paths.each do |path|
458: ["_#{filename}", filename].each do |name|
459: full_path = File.join(path, name)
460: if File.readable?(full_path)
461: return full_path
462: end
463: end
464: end
465: nil
466: end
Processes the template and returns the result as a string.
# File lib/sass/engine.rb, line 86
86: def render
87: begin
88: render_to_tree.to_s
89: rescue SyntaxError => err
90: unless err.sass_filename
91: err.add_backtrace_entry(@options[:filename])
92: end
93: raise err
94: end
95: end
# File lib/sass/engine.rb, line 109
109: def render_to_tree
110: split_lines
111:
112: root = Tree::Node.new(@options[:style])
113: index = 0
114: while @lines[index]
115: old_index = index
116: child, index = build_tree(index)
117:
118: if child.is_a? Tree::Node
119: child.line = old_index + 1
120: root << child
121: elsif child.is_a? Array
122: child.each do |c|
123: root << c
124: end
125: end
126: end
127: @lines.clear
128:
129: root
130: end
# File lib/sass/engine.rb, line 185
185: def build_tree(index)
186: line, tabs = @lines[index]
187: index += 1
188: @line = index
189: node = parse_line(line)
190:
191: has_children = has_children?(index, tabs)
192:
193: # Node is a symbol if it's non-outputting, like a constant assignment
194: unless node.is_a? Tree::Node
195: if has_children
196: if node == :constant
197: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line + 1)
198: elsif node.is_a? Array
199: # arrays can either be full of import statements
200: # or attributes from mixin includes
201: # in either case they shouldn't have children.
202: # Need to peek into the array in order to give meaningful errors
203: directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin")
204: raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line + 1)
205: end
206: end
207:
208: index = @line if node == :mixin
209: return node, index
210: end
211:
212: node.line = @line
213:
214: if node.is_a? Tree::CommentNode
215: while has_children
216: line, index = raw_next_line(index)
217: node << line
218:
219: has_children = has_children?(index, tabs)
220: end
221:
222: return node, index
223: end
224:
225: # Resolve multiline rules
226: if node.is_a?(Tree::RuleNode)
227: if node.continued?
228: child, index = build_tree(index) if @lines[old_index = index]
229: if @lines[old_index].nil? || has_children?(old_index, tabs) || !child.is_a?(Tree::RuleNode)
230: raise SyntaxError.new("Rules can't end in commas.", @line)
231: end
232:
233: node.add_rules child
234: end
235: node.children = child.children if child
236: end
237:
238: while has_children
239: child, index = build_tree(index)
240:
241: validate_and_append_child(node, child)
242:
243: has_children = has_children?(index, tabs)
244: end
245:
246: return node, index
247: end
Counts the tabulation of a line.
# File lib/sass/engine.rb, line 166
166: def count_tabs(line)
167: return nil if line.strip.empty?
168: return nil unless spaces = line.index(/[^ ]/)
169:
170: if spaces % 2 == 1
171: raise SyntaxError.new("\#{spaces} space\#{spaces == 1 ? ' was' : 's were'} used for indentation. Sass must be indented using two spaces.\n".strip, @line)
172: elsif line[spaces] == ?\t
173: raise SyntaxError.new("A tab character was used for indentation. Sass must be indented using two spaces.\nAre you sure you have soft tabs enabled in your editor?\n".strip, @line)
174: end
175: spaces / 2
176: end
# File lib/sass/engine.rb, line 267
267: def has_children?(index, tabs)
268: next_line = ['//', 0]
269: while !next_line.nil? && next_line[0] == '//' && next_line[1] = 0
270: next_line = @lines[index]
271: index += 1
272: end
273: next_line && next_line[1] > tabs
274: end
# File lib/sass/engine.rb, line 396
396: def import(files)
397: nodes = []
398:
399: files.split(/,\s*/).each do |filename|
400: engine = nil
401:
402: begin
403: filename = self.class.find_file_to_import(filename, @options[:load_paths])
404: rescue Exception => e
405: raise SyntaxError.new(e.message, @line)
406: end
407:
408: if filename =~ /\.css$/
409: nodes << Tree::DirectiveNode.new("@import url(#{filename})", @options[:style])
410: else
411: File.open(filename) do |file|
412: new_options = @options.dup
413: new_options[:filename] = filename
414: engine = Sass::Engine.new(file.read, @options)
415: end
416:
417: engine.constants.merge! @constants
418: engine.mixins.merge! @mixins
419:
420: begin
421: root = engine.render_to_tree
422: rescue Sass::SyntaxError => err
423: err.add_backtrace_entry(filename)
424: raise err
425: end
426: root.children.each do |child|
427: child.filename = filename
428: nodes << child
429: end
430: @constants = engine.constants
431: @mixins = engine.mixins
432: end
433: end
434:
435: nodes
436: end
# File lib/sass/engine.rb, line 315
315: def parse_attribute(line, attribute_regx)
316: if @options[:attribute_syntax] == :normal &&
317: attribute_regx == ATTRIBUTE_ALTERNATE
318: raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
319: elsif @options[:attribute_syntax] == :alternate &&
320: attribute_regx == ATTRIBUTE
321: raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
322: end
323:
324: name, eq, value = line.scan(attribute_regx)[0]
325:
326: if name.nil? || value.nil?
327: raise SyntaxError.new("Invalid attribute: \"#{line}\".", @line)
328: end
329:
330: if eq.strip[0] == SCRIPT_CHAR
331: value = Sass::Constant.parse(value, @constants, @line).to_s
332: end
333:
334: Tree::AttrNode.new(name, value, @options[:style])
335: end
# File lib/sass/engine.rb, line 353
353: def parse_comment(line)
354: if line[1] == SASS_COMMENT_CHAR
355: :comment
356: elsif line[1] == CSS_COMMENT_CHAR
357: Tree::CommentNode.new(line, @options[:style])
358: else
359: Tree::RuleNode.new(line, @options[:style])
360: end
361: end
# File lib/sass/engine.rb, line 337
337: def parse_constant(line)
338: name, op, value = line.scan(Sass::Constant::MATCH)[0]
339: unless name && value
340: raise SyntaxError.new("Invalid constant: \"#{line}\".", @line)
341: end
342:
343: constant = Sass::Constant.parse(value, @constants, @line)
344: if op == '||='
345: @constants[name] ||= constant
346: else
347: @constants[name] = constant
348: end
349:
350: :constant
351: end
# File lib/sass/engine.rb, line 363
363: def parse_directive(line)
364: directive, value = line[1..-1].split(/\s+/, 2)
365:
366: # If value begins with url( or ",
367: # it's a CSS @import rule and we don't want to touch it.
368: if directive == "import" && value !~ /^(url\(|")/
369: import(value)
370: else
371: Tree::DirectiveNode.new(line, @options[:style])
372: end
373: end
# File lib/sass/engine.rb, line 280
280: def parse_line(line)
281: case line[0]
282: when ATTRIBUTE_CHAR
283: if line[1] != ATTRIBUTE_CHAR
284: parse_attribute(line, ATTRIBUTE)
285: else
286: # Support CSS3-style pseudo-elements,
287: # which begin with ::
288: Tree::RuleNode.new(line, @options[:style])
289: end
290: when Constant::CONSTANT_CHAR
291: parse_constant(line)
292: when COMMENT_CHAR
293: parse_comment(line)
294: when DIRECTIVE_CHAR
295: parse_directive(line)
296: when ESCAPE_CHAR
297: Tree::RuleNode.new(line[1..-1], @options[:style])
298: when MIXIN_DEFINITION_CHAR
299: parse_mixin_definition(line)
300: when MIXIN_INCLUDE_CHAR
301: if line[1].nil? || line[1] == ?\s
302: Tree::RuleNode.new(line, @options[:style])
303: else
304: parse_mixin_include(line)
305: end
306: else
307: if line =~ ATTRIBUTE_ALTERNATE_MATCHER
308: parse_attribute(line, ATTRIBUTE_ALTERNATE)
309: else
310: Tree::RuleNode.new(line, @options[:style])
311: end
312: end
313: end
# File lib/sass/engine.rb, line 375
375: def parse_mixin_definition(line)
376: mixin_name = line[1..-1].strip
377: @mixins[mixin_name] = []
378: index = @line
379: line, tabs = @lines[index]
380: while !line.nil? && tabs > 0
381: child, index = build_tree(index)
382: validate_and_append_child(@mixins[mixin_name], child)
383: line, tabs = @lines[index]
384: end
385: :mixin
386: end
# File lib/sass/engine.rb, line 388
388: def parse_mixin_include(line)
389: mixin_name = line[1..-1]
390: unless @mixins.has_key?(mixin_name)
391: raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
392: end
393: @mixins[mixin_name]
394: end
# File lib/sass/engine.rb, line 276
276: def raw_next_line(index)
277: [@lines[index][0], index + 1]
278: end
Readies each line in the template for parsing, and computes the tabulation of the line.
# File lib/sass/engine.rb, line 136
136: def split_lines
137: @line = 0
138: old_tabs = nil
139: @template.each_with_index do |line, index|
140: @line += 1
141:
142: tabs = count_tabs(line)
143:
144: if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
145: tabs = old_tabs
146: end
147:
148: if tabs # if line isn't blank
149: raise SyntaxError.new("Indenting at the beginning of the document is illegal.", @line) if old_tabs.nil? && tabs > 0
150:
151: if old_tabs && tabs - old_tabs > 1
152: raise SyntaxError.new("#{tabs * 2} spaces were used for indentation. Sass must be indented using two spaces.", @line)
153: end
154: @lines << [line.strip, tabs]
155:
156: old_tabs = tabs
157: else
158: @lines << ['//', old_tabs || 0]
159: end
160: end
161:
162: @line = nil
163: end
# File lib/sass/engine.rb, line 249
249: def validate_and_append_child(parent, child)
250: case child
251: when :constant
252: raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
253: when :mixin
254: raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line)
255: when Array
256: child.each do |c|
257: if c.is_a?(Tree::DirectiveNode)
258: raise SyntaxError.new("Import directives may only be used at the root of a document.", @line)
259: end
260: parent << c
261: end
262: when Tree::Node
263: parent << child
264: end
265: end