class Chance::Parser

Constants

BEGIN_SCOPE
CHANCE_FILE_DIRECTIVE
END_SCOPE
INCLUDE_SLICES_DIRECTIVE
INCLUDE_SLICE_DIRECTIVE
NORMAL_SCAN_UNTIL
SELECTOR_THEME_VARIABLE
THEME_DIRECTIVE
UNTIL_DOUBLE_QUOTE
UNTIL_SINGLE_QUOTE

Attributes

css[R]
slices[R]

Public Class Methods

new(string, opts = {}) click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 57
def initialize(string, opts = {})
  @opts = { :theme => "" }
  @opts.merge!(opts)
  @path = ""


  @input = string
  @css = ""

  @slices = @opts[:slices]  # we update the slices given to us

  @theme = @opts[:theme]

end

Public Instance Methods

_parse() click to toggle source

_parse will parse until it finds either the end or until it finds an unmatched ending brace. An unmatched ending brace is assumed to mean that this is a recursive call.

# File vendor/chance/lib/chance/parser.rb, line 193
def _parse
  scanner = @scanner

  output = []

  while not scanner.eos? do
    output << handle_empty
    break if scanner.eos?

    if scanner.match?(BEGIN_SCOPE)
      output << handle_scope
      next
    end

    if scanner.match?(THEME_DIRECTIVE)
      output << handle_theme
      next
    end

    if scanner.match?(SELECTOR_THEME_VARIABLE)
      output << handle_theme_variable
      next
    end

    if scanner.match?(INCLUDE_SLICES_DIRECTIVE)
      output << handle_slices
      next
    end

    if scanner.match?(INCLUDE_SLICE_DIRECTIVE)
      output << handle_slice_include
      next
    end

    if scanner.match?(CHANCE_FILE_DIRECTIVE)
      handle_file_change
      next
    end

    break if scanner.match?(END_SCOPE)

    # skip over anything that our tokens do not start with
    res = scanner.scan(NORMAL_SCAN_UNTIL)
    if res.nil?
      output << scanner.getch
    else
      output << res
    end

  end

  output = output.join
  return output
end
create_slice(opts) click to toggle source

SLICE MANAGEMENT


# File vendor/chance/lib/chance/parser.rb, line 74
def create_slice(opts)
  filename = opts[:filename]

  # get current relative path
  relative = File.dirname(@path)

  # Create a path
  path = File.join(relative, filename)
  path = path[2..-1] if path[0,2] == "./"
  path = Pathname.new(path).cleanpath.to_s

  opts = opts.merge({ :path => path })
  opts = normalize_rectangle(opts)

  slice_path = path[0..-File.extname(filename).length-1]

  # we add a bit to the path: the slice info
  rect_params = [:left, :top, :width, :height, :bottom, :right, :offset_x, :offset_y]

  # Generate string-compatible params
  slice_name_params = rect_params.map {|param|
    ret = ""
    ret = opts[param] if not opts[param].nil?
    ret
  }
  slice_name_params.unshift slice_path

  # validate and convert to integers
  rect_params.each {|param|
    value = opts[param]
    if not value.nil?
      value = Integer(value)
      opts[param] = value
    end
  }



  # it is too expensive to open the images and get their sizes at this point, though
  # I rather would like to.transform the rectangle into absolute coordinates
  # (left top width height) and use that instead of showing all six digits.
  slice_path = "%s_%s_%s_%s_%s_%s_%s" % slice_name_params

  if @slices.has_key?(slice_path)
    slice = @slices[slice_path]
    slice[:min_offset_x] = [slice[:min_offset_x], opts[:offset_x]].min
    slice[:min_offset_y] = [slice[:min_offset_y], opts[:offset_y]].min

    slice[:max_offset_x] = [slice[:max_offset_x], opts[:offset_x]].max
    slice[:max_offset_y] = [slice[:max_offset_y], opts[:offset_y]].max
  else
    modified_path = @opts[:instance_id].to_s.gsub(/[^a-zA-Z0-9]/, '_') + "_" + slice_path.gsub(/[^a-zA-Z0-9]/, '_')
    css_name = "__chance_slice_#{modified_path}"

    slice = opts.merge({ 
      :name => slice_path, 
      :path => path,
      :css_name => css_name,
      :min_offset_x => opts[:offset_x], # these will be taken into account when spriting.
      :min_offset_y => opts[:offset_y],
      :max_offset_x => opts[:offset_x],
      :max_offset_y => opts[:offset_y],
      :imaged_offset_x => 0, # the imaging process will re-define these.
      :imaged_offset_y => 0,

      :used_by => []
    })

    @slices[slice_path] = slice
  end

  slice[:used_by] << { :path => @path }

  return slice
end
generate_slice_include(slice) click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 453
def generate_slice_include(slice)
  # the argument list is rather raw. We need to combine it with default values,
  # and preprocess any arguments, before we can call create_slice to get the real
  # slice definition
  slice[:offset] = "0 0" if slice[:offset].nil?
  slice[:repeat] = "no-repeat" if slice[:repeat].nil?

  # the offset will be given to us as one string; however, it has two parts.
  # splitting by whitespace doesn't handle everything, so we may want to refine
  # this at some point unless we could just pass the whole offset to the offset
  # function somehow.
  offset = slice[:offset].strip.split(/\s+/)
  slice[:offset_x] = offset[0]
  slice[:offset_y] = offset[1]

  slice = create_slice(slice)


  output = ""
  output += "@extend ." + slice[:css_name] + ";\n"

  # We prefix with -chance; this should let everything be passed through more
  # or less as-is. Postprocessing will turn it into -background-position.
  output += "-chance-offset: \"#{slice[:name]}\" #{offset[0]} #{offset[1]};"
  output += "background-repeat: " + slice[:repeat]
  return output
end
handle_comment() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 248
def handle_comment
  scanner = @scanner
  scanner.pos += 2
  scanner.scan_until /\*\//
end
handle_empty() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 278
def handle_empty
  scanner = @scanner
  output = ""

  while true do
    if scanner.match?(/\s+/)
      output += scanner.scan /\s+/
      next
    end
    if scanner.match?(/\/\//)
      scanner.scan_until /\n/
      next
    end
    if scanner.match?(/\/\*/)
      handle_comment
      next
    end
    break
  end

  return output
end
handle_file_change() click to toggle source

when we receive a @_chance_file directive, it means that our current file scope has changed. We need to know this because we parse the combined file rather than the individual pieces, yet we have paths relative to the original files.

# File vendor/chance/lib/chance/parser.rb, line 357
def handle_file_change
  scanner = @scanner
  scanner.scan CHANCE_FILE_DIRECTIVE

  path = scanner.scan_until /;/
  path = path[0..-1]

  @path = path
end
handle_scope() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 301
def handle_scope
  scanner = @scanner

  scanner.scan /\{/

  output = '{'
  output += _parse
  output += '}'

  raise SyntaxError, "Expected end of block." unless scanner.scan /\}/

  return output
end
handle_slice_include() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 481
def handle_slice_include
  scanner = @scanner
  scanner.scan /@include slice\s*/

  slice = parse_argument_list

  # the image could be quoted or not; in any case, use parse_string to
  # parse it. Sure, at the moment, we don't parse quoted strings properly,
  # but it should work for most cases. single-quoted strings are out, though...
  slice[:filename] = parse_string(slice[0])

  # now that we have all of the info, we can get the actual slice information.
  # This process will create a slice entry if needed.
  return generate_slice_include(slice)
end
handle_slices() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 529
def handle_slices
  scanner = @scanner
  scanner.scan /@include slices\s*/

  arguments = parse_argument_list

  # slices() only supports four-param, left top right bottom rectangles.
  [:top, :left, :bottom, :right].each {|key|
    arguments[key] = Integer(arguments[key]) if not arguments[key].nil?
    arguments[key] = 0 if arguments[key].nil?
  }

  values = arguments.values

  left = arguments[:left]
  top = arguments[:top]
  right = arguments[:right]
  bottom = arguments[:bottom]

  # determine fill method
  fill = arguments[:fill] || "1 0"
  fill = fill.strip.split(/\s+/)
  fill_width = Integer(fill[0])
  fill_height = Integer(fill[1])

  # skip control
  skip = arguments[:skip]
  if skip.nil?
    skip = []
  else
    skip = skip.split /\s+/
  end

  skip_top_left = skip.include? 'top-left'
  skip_top = skip.include? 'top'
  skip_top_right = skip.include? 'top-right'

  skip_left = skip.include? 'left'
  skip_middle = skip.include? 'middle'
  skip_right = skip.include? 'right'

  skip_bottom_left = skip.include? 'bottom-left'
  skip_bottom = skip.include? 'bottom'
  skip_bottom_right = skip.include? 'bottom-right'

  filename = parse_string(arguments[0])

  # we are going to form 9 slices. If any are empty we'll skip them

  # top-left
  top_left_slice = {
    :left => 0,
    :top => 0,
    :width => left,
    :height => top,
    :sprite_anchor => arguments[:"top-left-anchor"],
    :sprite_padding => arguments[:"top-left-padding"],
    :offset => arguments[:"top-left-offset"],
    :filename => filename
  }

  left_slice = {
    :left => 0,
    :top => top,
    :width => left,

    :sprite_anchor => arguments[:"left-anchor"],
    :sprite_padding => arguments[:"left-padding"],
    :offset => arguments[:"left-offset"],
    :filename => filename,
    :repeat => fill_height == 0 ? nil : "repeat-y"


    # we fill in either height or bottom, depending on fill
  }

  bottom_left_slice = {
    :left => 0,
    :bottom => 0,
    :width => left,
    :height => bottom,

    :sprite_anchor => arguments[:"bottom-left-anchor"],
    :sprite_padding => arguments[:"bottom-left-padding"],
    :offset => arguments[:"bottom-left-offset"],
    :filename => filename


  }

  top_slice = {
    :left => left,
    :top => 0,
    :height => top,

    :sprite_anchor => arguments[:"top-anchor"],
    :sprite_padding => arguments[:"top-padding"],
    :offset => arguments[:"top-offset"],
    :filename => filename,
    :repeat => fill_width == 0 ? nil : "repeat-x"



    # we fill in either width or right, depending on fill
  }

  middle_slice = {
    :left => left,
    :top => top,

    :sprite_anchor => arguments[:"middle-anchor"],
    :sprite_padding => arguments[:"middle-padding"],
    :offset => arguments[:"middle-offset"],
    :filename => filename,
    :repeat => fill_height != 0 ? (fill_width != 0 ? "repeat" : "repeat-y") : (fill_width != 0 ? "repeat-x" : nil)


    # fill in width, height or right, bottom depending on fill settings
  }

  bottom_slice = {
    :left => left,
    :bottom => 0,
    :height => bottom,

    :sprite_anchor => arguments[:"bottom-anchor"],
    :sprite_padding => arguments[:"bottom-padding"],
    :offset => arguments[:"bottom-offset"],
    :filename => filename,
    :repeat => fill_width == 0 ? nil : "repeat-x"



    # we fill in width or right depending on fill settings
  }

  top_right_slice = {
    :right => 0,
    :top => 0,
    :width => right,
    :height => top,

    :sprite_anchor => arguments[:"top-right-anchor"],
    :sprite_padding => arguments[:"top-right-padding"],
    :offset => arguments[:"top-right-offset"],
    :filename => filename


  }

  right_slice = {
    :right => 0,
    :top => top,
    :width => right,

    :sprite_anchor => arguments[:"right-anchor"],
    :sprite_padding => arguments[:"right-padding"],
    :offset => arguments[:"right-offset"],
    :filename => filename,

    :repeat => fill_height == 0 ? nil : "repeat-y"


    # we fill in either height or top depending on fill settings
  }

  bottom_right_slice = {
    :right => 0,
    :bottom => 0,
    :width => right,
    :height => bottom,

    :sprite_anchor => arguments[:"bottom-right-anchor"],
    :sprite_padding => arguments[:"bottom-right-padding"],
    :offset => arguments[:"bottom-right-offset"],
    :filename => filename


  }

  if fill_width == 0
    top_slice[:right] = right
    middle_slice[:right] = right
    bottom_slice[:right] = right
  else
    top_slice[:width] = fill_width
    middle_slice[:width] = fill_width
    bottom_slice[:width] = fill_width
  end

  if fill_height == 0
    left_slice[:bottom] = bottom
    middle_slice[:bottom] = bottom
    right_slice[:bottom] = bottom
  else
    left_slice[:height] = fill_height
    middle_slice[:height] = fill_height
    right_slice[:height] = fill_height
  end

  output = ""

  # LEFT
  # NOTE: we write it even if we are supposed to skip; we only wrap the slice include portion in
  if should_include_slice?(top_left_slice)
    output += "& > .top-left {\n"

    # IF we are skipping the top left slice, we don't want the actual slice include-- but the layout
    # information we still want. So, only bracket the generate_slice_include.
    #
    # Potential issue: slice_layout doesn't handle repeat settings. In theory, this should rarely be
    # an issue (because you aren't setting a background for the skipped slice.
    if not skip_top_left
      output += generate_slice_include(top_left_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(top_left_slice)

    output += "}\n"
  end

  if should_include_slice?(left_slice)
    output += "& > .left {\n"

    if not skip_left
      output += generate_slice_include(left_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(left_slice.merge({ :bottom => bottom }))

    output += "}\n"
  end

  if should_include_slice?(bottom_left_slice)
    output += "& > .bottom-left {\n"

    if not skip_bottom_left
      output += generate_slice_include(bottom_left_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(bottom_left_slice)

    output += "}\n"
  end

  # MIDDLE
  if should_include_slice?(top_slice)
    output += "& > .top {\n"

    if not skip_top
      output += generate_slice_include(top_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(top_slice.merge({ :right => right }))

    output += "}\n"
  end

  if should_include_slice?(middle_slice)
    output += "& > .middle {\n"

    if not skip_middle
      output += generate_slice_include(middle_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(middle_slice.merge({ :bottom => bottom, :right => right }))


    output += "}\n"
  end

  if should_include_slice?(bottom_slice)
    output += "& > .bottom {\n"

    if not skip_bottom
      output += generate_slice_include(bottom_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(bottom_slice.merge({ :right => right }))


    output += "}\n"
  end

  # RIGHT
  if should_include_slice?(top_right_slice)
    output += "& > .top-right {\n"

    if not skip_top_right
      output += generate_slice_include(top_right_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(top_right_slice)

    output += "}\n"
  end

  if should_include_slice?(right_slice)
    output += "& > .right {\n"

    if not skip_right
      output += generate_slice_include(right_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(right_slice.merge({ :bottom => bottom }))

    output += "}\n"
  end

  if should_include_slice?(bottom_right_slice)
    output += "& > .bottom-right {\n"

    if not skip_bottom_right
      output += generate_slice_include(bottom_right_slice) + ";"
    end

    output += "\nposition: absolute;\n"
    output += slice_layout(bottom_right_slice)

    output += "}\n"
  end

  return output
end
handle_string() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 269
def handle_string
  scanner = @scanner

  str = scanner.getch
  str += scanner.scan_until(str == "'" ? UNTIL_SINGLE_QUOTE : UNTIL_DOUBLE_QUOTE)

  return str
end
handle_theme() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 315
def handle_theme
  scanner = @scanner
  scanner.scan THEME_DIRECTIVE

  if scanner.scan(/\((.+?)\)\s*/).nil?
    raise SyntaxError, "Expected (theme-name) after @theme"
  end

  theme_name = scanner[1]

  raise SyntaxError, "Expected { after @theme." unless scanner.scan /\{/

  # calculate new theme name
  old_theme = @theme
  @theme = old_theme + "." + theme_name


  output = ""
  output += "\n$theme: '" + @theme + "';\n"
  output += _parse

  @theme = old_theme
  output += "$theme: '" + @theme + "';\n"

  raise SyntaxError, "Expected end of block." unless scanner.scan /\}/

  return output
end
handle_theme_variable() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 344
def handle_theme_variable
  scanner = @scanner
  scanner.scan SELECTOR_THEME_VARIABLE

  output = "\#{$theme}"

  return output
end
normalize_rectangle(rect) click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 150
def normalize_rectangle(rect)
  # try to make the rectangle somewhat standard: that is, make it have
  # all units which make sense

  # it must have either a left or a right, no matter what
  rect[:left] = 0 if rect[:left].nil? and rect[:right].nil?

  # if there is no width, it must have a both left and right
  if rect[:width].nil?
    rect[:left] = 0 if rect[:left].nil?
    rect[:right] = 0 if rect[:right].nil?
  end

  # it must have either a top or a bottom, no matter what
  rect[:top] = 0 if rect[:top].nil? and rect[:bottom].nil?

  # if there is no height, it must have _both_ top and bottom
  if rect[:height].nil?
    rect[:top] = 0 if rect[:top].nil?
    rect[:bottom] = 0 if rect[:bottom].nil?
  end

  return rect
end
parse() click to toggle source

PARSING


# File vendor/chance/lib/chance/parser.rb, line 177
def parse
  @scanner = StringScanner.new(@input)

  @image_names = {}
  @css = _parse

  if not @scanner.eos?
    # how do we do an error?
    raise SyntaxError, "Found end of block; expecting end of file."
  end

end
parse_argument() click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 367
def parse_argument
  scanner = @scanner

  # We do not care for whitespace or comments
  handle_empty

  # this is the final value; we won't actually set it until
  # the very end.
  value = nil

  # this holds the value as we are parsing it
  parsing_value = ""


  # The key MAY be present if we are starting with a $.
  # But remember: it could be $abc: $abc + $def
  key = :NO_KEY
  if scanner.match?(/\$/)
    scanner.scan /\$/

    handle_empty
    parsing_value = scanner.scan(/[a-zA-Z_-][a-zA-Z0-9+_-]*/)

    raise SyntaxError, "Expected a valid key." if key.nil?

    handle_empty

    if scanner.scan(/:/)
      # ok, it was a key
      key = parsing_value.intern
      parsing_value = ""

      handle_empty
    end
  end

  value = nil

  # we stop when we either a) reach the end of the arglist, or
  # b) reach the end of the argument. Argument ends at ',', list ends
  # at ')'
  parsing_value += handle_empty

  until scanner.match?(/[,)]/) or scanner.eos? do
    if scanner.match?(/["']/)
      parsing_value += handle_string
      parsing_value += handle_empty
      next
    end

    parsing_value += scanner.getch

    parsing_value += handle_empty
  end

  value = parsing_value unless parsing_value.empty?

  return { :key => key, :value => value }
end
parse_argument_list() click to toggle source

Parses a list of arguemnts, INCLUDING beginning AND ending parenthesis.

# File vendor/chance/lib/chance/parser.rb, line 429
def parse_argument_list
  scanner = @scanner

  raise SyntaxError, "Expected ( to begin argument list." unless scanner.scan /\(/

  idx = 0
  args = {}
  until scanner.match?(/\)/) or scanner.eos? do
    arg = parse_argument
    if arg[:key] == :NO_KEY
      arg[:key] = idx
      idx += 1
    end

    args[arg[:key]] = arg[:value].strip

    scanner.scan /,/
  end

  scanner.scan /\)/

  return args
end
parse_string(cssString) click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 254
def parse_string(cssString)
  # I cheat: to parse strings, I use JSON.
  if cssString[0..0] == "'"
    # We should still be able to use json to parse single-quoted strings
    # if we replace the quotes with double-quotes. The methodology should
    # be identical so long as we replace any unescaped quotes...
    cssString = '"' + cssString[1..-2].gsub(/^"|([^\\]")/, '\1\\"') + '"'

  elsif not cssString[0..0] == '"'
    return cssString
  end

  return JSON.parse("[" + cssString + "]").first
end
should_include_slice?(slice) click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 497
def should_include_slice?(slice)
  return true if slice[:width].nil?
  return true if slice[:height].nil?

  return false if slice[:width] == 0
  return false if slice[:height] == 0

  return true
end
slice_layout(slice) click to toggle source
# File vendor/chance/lib/chance/parser.rb, line 507
def slice_layout(slice)
  output = ""

  layout_properties = [:left, :top, :right, :bottom]

  if slice[:right].nil? or slice[:left].nil?
    layout_properties.push(:width)
  end

  if slice[:bottom].nil? or slice[:top].nil?
    layout_properties.push(:height)
  end

  layout_properties.each {|prop|
    unless slice[prop].nil?
      output += prop.to_s + ": " + slice[prop].to_s + "px; \n"
    end
  }

  return output
end