TinyCSS

http://d.hatena.ne.jp/milk1000cc/20080703を読んで、自分なりに
いじってみた。

class TinyCSS
  class ParseError < ::RuntimeError ; end
  
  def self.read(filename)
    new().parse(File.read(filename))
  end
  
  def self.parse(str)
    new().parse(str)
  end
  
  def initialize
    @style = Hash.new{|h, k| h[k] = {} }
  end
  
  attr_reader :style
  
  def parse(str)
    str = str.tr("\n\t", '  ').gsub(/\/\*.*?\*\//, '')
    blocks = str.split('}').map{|b| b.strip }
    raise ParseError, 'invalid style sheet' unless blocks.last.empty?
    blocks.each do |block|
      next if block.empty?
      selecters, brace, inner = block.partition('{')
      raise ParseError, "invalid style description - `#{block}'" if brace.empty?
      selecters = selecters.split(',').map{|s| s.strip }
      inner.strip.split(';').each do |desc|
        attribute, colon, value = desc.partition(':')
        raise ParseError, "invalid style description - `#{desc}'" if colon.empty?
        attribute, value = attribute.strip, value.strip
        raise ParseError, "invalid attribute - `#{attribute}'" unless /\A[\w.-]+\Z/ =~ attribute
        raise ParseError, "missing value for `#{attribute}'" if value.empty?
        selecters.each do |selecter|
          style[selecter][attribute] = value
        end
      end
    end
    self
  end
  
  def dump(f = nil)
    buf = []
    style.keys.sort.reverse_each do |selecter|
      buf << "#{selecter} {"
      style[selecter].keys.sort.each do |attribute|
        buf << "\t#{attribute}: #{style[selecter][attribute]};"
      end
      buf << "}"
    end
    str = buf.join("\n")
    f ? f << str : str
  end
end


if __FILE__ == $0
  css = TinyCSS.read('test.css')
  puts css.dump
end

ただ、そもそも自分でファイルに変更を書き戻すのがめんどい
感じになってきたのでしょぼいDB風にしてみた。

require 'fileutils'

class TinyCSS
  class Error < ::RuntimeError ; end
  class AbortError < Error ; end
  class ParseError < Error ; end
  
  def self.open(filename, &block)
    css = new(filename)
    return css unless block_given?
    begin
      yield(css)
      css.commit
    rescue AbortError
      ;
    ensure
      css.close
    end
    nil
  end
  
  def initialize(filename)
    begin
      @f = File.open(filename, 'r+')
    rescue Errno::ENOENT
      FileUtils.touch(filename)
      retry
    end
    @f.flock(File::LOCK_EX)
    @style = Hash.new{|h, k| h[k] = {} }
    @f.rewind
    parse(@f.read)
  end
  
  attr_reader :style
  
  def close
    @f.close
  end
  
  def commit
    @f.rewind
    dump(@f)
    @f.truncate(@f.tell)
  end
  
  def abort
    raise AbortError, 'transaction aborted'
  end
  
  private
  
  def parse(str)
    str = str.tr("\n\t", '  ').gsub(/\/\*.*?\*\//, '')
    blocks = str.split('}').map{|b| b.strip }
    return self if blocks.empty?
    raise ParseError, 'invalid style sheet' unless blocks.last.empty?
    blocks.each do |block|
      next if block.empty?
      selecters, brace, inner = block.partition('{')
      raise ParseError, "invalid style description - `#{block}'" if brace.empty?
      selecters = selecters.split(',').map{|s| s.strip }
      inner.strip.split(';').each do |desc|
        attribute, colon, value = desc.partition(':')
        raise ParseError, "invalid style description - `#{desc}'" if colon.empty?
        attribute, value = attribute.strip, value.strip
        raise ParseError, "invalid attribute - `#{attribute}'" unless /\A[\w.-]+\Z/ =~ attribute
        raise ParseError, "missing value for `#{attribute}'" if value.empty?
        selecters.each do |selecter|
          style[selecter][attribute] = value
        end
      end
    end
    self
  end
  
  def dump(f = nil)
    buf = []
    style.keys.sort.reverse_each do |selecter|
      buf << "#{selecter} {"
      style[selecter].keys.sort.each do |attribute|
        buf << "\t#{attribute}: #{style[selecter][attribute]};"
      end
      buf << "}"
      buf << ''
    end
    str = buf.join("\n")
    f ? f << str : str
  end
end


if __FILE__ == $0
  TinyCSS.open('hoge.css') do |css|
    css.style['a']['background'] = 'black'
  end
  TinyCSS.open('hoge.css') do |css|
    css.style['a']['background'] = 'blue'
    css.abort
  end
  puts File.read('hoge.css')
end
# >> a {
# >> 	background: black;
# >> }