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; # >> }