module Ass class Parser OPEN_BRACE = " {\n" CLOSING_BRACE = "}\n" AT = /^ *@/ COMMENT = /^ *\/\*(.*)(?:\*\/)?$/ ASS_COMMENT = /&\/\// BLANK = /^ *$/ # TODO: Single \n char as well, update test.ass WORD = /([\w]+)/ HYPHENATED_WORD = /([\w\-]+)/ MIXIN = /^ *\+#{WORD}/ SELECTOR = /^([#\w\-\.,:\[\]\~\* ]+) *$/ # TODO: review CSS specification EQUALS = / *= */ CONSTANT = /:#{WORD}/ CONSTANT_VALUE = /([#\w]+)/ CONSTANT_WITH_VALUE = /^ *#{CONSTANT}#{EQUALS}#{CONSTANT_VALUE}/ PROPERTY = /^ *#{HYPHENATED_WORD}/ attr_accessor :ass, :constants, :css alias :to_s :ass def initialize ass = '' @ass, @css, @constants = ass, '', {} end def parse! mixins = Hash.new '' in_block, in_mixin = false, false last_selector = '' lines = @ass.split "\n" lines.each do |line| if line.match COMMENT @css << "/* #{$1.strip} */\n" elsif line.match CONSTANT_WITH_VALUE @constants[$1.to_sym] = $2 elsif not in_block and line.match MIXIN in_mixin, in_block = $1, true elsif in_block and line.match MIXIN next if in_mixin @css << mixins[$1] elsif in_block and line.match BLANK in_block = false next if in_mixin @css << CLOSING_BRACE elsif line.match AT @css << line << "\n" elsif line.match BLANK or line.match ASS_COMMENT # Do nothing elsif not in_block and line.match SELECTOR in_block = true if sub_selector?($1, last_selector) next if in_mixin selector = join_selectors last_selector, $1.strip @css << selector << OPEN_BRACE else in_mixin = false selector = $1.strip @css << selector << OPEN_BRACE end last_selector = selector elsif line.match PROPERTY if mixin = in_mixin mixins[mixin] << format_property(line) else @css << format_property(line) end end end self end def join_selectors selector_a, selector_b if selector_b[0,1] == ':' if selector_a.include? ',' selector_a.split(',').collect { |selector| selector << selector_b }.join(',') else selector_a << selector_b end else if selector_a.include? ',' selector_a.split(',').collect { |selector| selector << ' ' << selector_b }.join(',') else selector_a << ' ' << selector_b end end end def sub_selector? selector_a, selector_b true if indents_in(selector_a) > indents_in(selector_b) end def indents_in string string.match(/^( *)/).captures.first.length / 2 rescue 0 end def format_property line line.gsub! PROPERTY, ' \1:' line.gsub! CONSTANT do |constant| constant_sym = constant.gsub(':', '').to_sym @constants[constant_sym] || 'undefined_constant' end line << ";\n" end def self.parse ass new(ass).parse!.css end def self.parse_file file new(File.read(file)).parse!.css end end end