views:

32

answers:

1

I'm creating an app where users can edit their own CSS (in SCSS syntax). That works fine, however, I eventually want these CSS files to be "programmable" so that users that don't know CSS can still edit them in a basic manner. How?

If I can mark certain things as editable, I don't have to make an impossible database schema. For example I have a scss file called style.scss:

// @type color
$header_bg_color: #555;

// @type image
$header_image: "http://someurl.com/image.jpg";

Then I can do this:

SomeParser.parse(contents of style.scss here)

This will return a hash or something similar of variables:

{:header_bg_color => {:type => "color", :value => "#555"}, :header_image => {:type => "image", :value => "http://someurl.com/image.jpg"} }

I can use the above hash to create a form which the novice user can use to change the data and submit. I believe I know how to do the GET and POST part.

What would be the best way to create / configure my own parser so that I could read the comments and extract the "variables" from this? And then, update the text file easily again?

Another possible way is something like this:

o = SomeParser.new(contents of style.scss here)
o.header_bg_color #returns "#555"
o.header_image = "http://anotherurl.com/image2.jpg" # "updates" or replaces the old header image variable with the new one
o.render # returns the text with the new values

Thanks in advance!

A: 

I haven't used it thoroughly, but my tests pass. I think it's enough to get the idea :) It took me several hours of study, then several more to implement it.

Btw, I did not do any optimization here. For me, it doesn't need to be quick

Look at my spec file:

require 'spec_helper'

describe StyleParser do
  describe "given properly formatted input" do
    it "should set and return variables properly" do
      text = %{# @name Masthead Background Image
# @kind file
# @description Background image.
$mbc2: "http://someurl.com/image.jpg";

# @name Masthead BG Color
# @kind color
# @description Background color.
$mbc: #555;}
      @s = StyleParser.new(text)

      @s.mbc.name.should == "Masthead BG Color"
      @s.mbc.kind.should == "color"
      @s.mbc.description.should == "Background color."
      @s.mbc.value.should == "#555"

      @s.mbc2.name.should == "Masthead Background Image"
      @s.mbc2.kind.should == "file"
      @s.mbc2.description.should == "Background image."
      @s.mbc2.value.should == %Q("http://someurl.com/image.jpg")
    end
  end

  describe "when assigning values" do
    it "should update its values" do
      text = %{# @name Masthead Background Image
# @kind file
# @description Background image.
$mbc2: "http://someurl.com/image.jpg";}
      @s = StyleParser.new(text)
      @s.mbc2.value = %Q("Another URL")
      @s.mbc2.value.should == %Q("Another URL")

      rendered_text = @s.render
      rendered_text.should_not match(/http:\/\/someurl\.com\/image\.jpg/)
      rendered_text.should match(/\$mbc2: "Another URL";/)

      @s.mbc2.value = %Q("Some third URL")
      @s.mbc2.value.should == %Q("Some third URL")
      rendered_text = @s.render
      rendered_text.should_not match(/\$mbc2: "Another URL";/)
      rendered_text.should match(/\$mbc2: "Some third URL";/)
    end

    it "should render the correct values" do
      text_old = %{# @name Masthead Background Image
# @kind file
# @description Background image.
$mbc2: "http://someurl.com/image.jpg";}
      text_new = %{# @name Masthead Background Image
# @kind file
# @description Background image.
$mbc2: "Another URL";}
      @s = StyleParser.new(text_old)
      @s.mbc2.value = %Q("Another URL")
      @s.render.should == text_new
    end
  end
end

Then the following 2 files:

# Used to parse through an scss stylesheet to make editing of that stylesheet simpler
# Ex. Given a file called style.scss
#
# // @name Masthead Background Color
# // @type color
# // @description Background color of the masthead.
# $masthead_bg_color: #444;
#
# sp = StyleParser.new(contents of style.scss)
#
# # Reading
# sp.masthead_bg_color.value # returns "#444"
# sp.masthead_bg_color.name # returns "Masthead Background Color"
# sp.masthead_bg_color.type # returns "color"
# sp.masthead_bg_color.description # returns "Background color of the masthead."
#
# # Writing
# sp.masthead_bg_color.value = "#555"
# sp.render # returns all the text above except masthead_bg_color is now #555;

class StyleParser
  def initialize(text)
    @text = text
    @variables = {}

    @eol = '\n'
    @context_lines = 3
    @context = "((?:.*#{@eol}){#{@context_lines}})"
  end

  # Works this way: http://rubular.com/r/jWSYvfVrjj
  # Derived from http://stackoverflow.com/questions/2760759/ruby-equivalent-to-grep-c-5-to-get-context-of-lines-around-the-match
  def get_context(s)
    regexp = /.*\${1}#{s}:.*;[#{@eol}]*/
    @text =~ /^#{@context}(#{regexp})/
    before, match = $1, $2
    "#{before}#{match}"
  end

  def render
    @variables.each do |key, var|
      @text.gsub!(/^\$#{key}: .+;/, %Q($#{key}: #{var.value};))
    end
    @text
  end

  def method_missing(method_name)
    if method_name.to_s =~ /[\w]+/
      context = get_context(method_name)

      @variables[method_name] ||= StyleVariable.new(method_name, context)
    end
  end
end
class StyleVariable
  METADATA = %w(name kind description)

  def initialize(var, text)
    @var = var
    @text = text
  end

  def method_missing(method_name)
    if METADATA.include? method_name.to_s
      content_of(method_name.to_s)
    end
  end

  def value
    @text.each do |string|
      string =~ /^\${1}#{@var}: (.+);/
      return $1 if $1
    end
  end

  def value=(val)
    @text.gsub!(/^\$#{@var}: .+;/, "$#{@var}: #{val};")
  end

  private

  def content_of(variable)
    @text.each do |string|
      string =~ /^# @([\w]+[^\s]) (.+)/
      return $2 if $1 == variable
    end
  end
end
Ramon Tayag