236 lines
6.6 KiB
Ruby
236 lines
6.6 KiB
Ruby
module ActionController
|
|
module CodeGeneration #:nodoc:
|
|
class GenerationError < StandardError #:nodoc:
|
|
end
|
|
|
|
class Source #:nodoc:
|
|
attr_reader :lines, :indentation_level
|
|
IndentationString = ' '
|
|
def initialize
|
|
@lines, @indentation_level = [], 0
|
|
end
|
|
def line(line)
|
|
@lines << (IndentationString * @indentation_level + line)
|
|
end
|
|
alias :<< :line
|
|
|
|
def indent
|
|
@indentation_level += 1
|
|
yield
|
|
ensure
|
|
@indentation_level -= 1
|
|
end
|
|
|
|
def to_s() lines.join("\n") end
|
|
end
|
|
|
|
class CodeGenerator #:nodoc:
|
|
attr_accessor :source, :locals
|
|
def initialize(source = nil)
|
|
@locals = []
|
|
@source = source || Source.new
|
|
end
|
|
|
|
BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
|
|
ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
|
|
Keywords = BeginKeywords + ResumeKeywords
|
|
|
|
def method_missing(keyword, *text)
|
|
if Keywords.include? keyword
|
|
if ResumeKeywords.include? keyword
|
|
raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/
|
|
source.lines.pop # Remove the 'end'
|
|
end
|
|
|
|
line "#{keyword} #{text.join ' '}"
|
|
begin source.indent { yield(self.dup) }
|
|
ensure line 'end'
|
|
end
|
|
else
|
|
super(keyword, *text)
|
|
end
|
|
end
|
|
|
|
def line(*args) self.source.line(*args) end
|
|
alias :<< :line
|
|
def indent(*args, &block) source(*args, &block) end
|
|
def to_s() source.to_s end
|
|
|
|
def share_locals_with(other)
|
|
other.locals = self.locals = (other.locals | locals)
|
|
end
|
|
|
|
FieldsToDuplicate = [:locals]
|
|
def dup
|
|
copy = self.class.new(source)
|
|
self.class::FieldsToDuplicate.each do |sym|
|
|
value = self.send(sym)
|
|
value = value.dup unless value.nil? || value.is_a?(Numeric)
|
|
copy.send("#{sym}=", value)
|
|
end
|
|
return copy
|
|
end
|
|
end
|
|
|
|
class RecognitionGenerator < CodeGenerator #:nodoc:
|
|
Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
|
|
attr_accessor(*Attributes)
|
|
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
|
|
|
|
def initialize(*args)
|
|
super(*args)
|
|
@after, @before = [], []
|
|
@current = nil
|
|
@results, @constants = {}, {}
|
|
@depth = 0
|
|
@move_ahead = nil
|
|
@finish_statement = Proc.new {|hash_expr| hash_expr}
|
|
end
|
|
|
|
def if_next_matches(string, &block)
|
|
test = Routing.test_condition(next_segment(true), string)
|
|
self.if(test, &block)
|
|
end
|
|
|
|
def move_forward(places = 1)
|
|
dup = self.dup
|
|
dup.depth += 1
|
|
dup.move_ahead = places
|
|
yield dup
|
|
end
|
|
|
|
def next_segment(assign_inline = false, default = nil)
|
|
if locals.include?(segment_name)
|
|
code = segment_name
|
|
else
|
|
code = "#{segment_name} = #{path_name}[#{index_name}]"
|
|
if assign_inline
|
|
code = "(#{code})"
|
|
else
|
|
line(code)
|
|
code = segment_name
|
|
end
|
|
|
|
locals << segment_name
|
|
end
|
|
code = "(#{code} || #{default.inspect})" if default
|
|
|
|
return code.to_s
|
|
end
|
|
|
|
def segment_name() "segment#{depth}".to_sym end
|
|
def path_name() :path end
|
|
def index_name
|
|
move_ahead, @move_ahead = @move_ahead, nil
|
|
move_ahead ? "index += #{move_ahead}" : 'index'
|
|
end
|
|
|
|
def continue
|
|
dup = self.dup
|
|
dup.before << dup.current
|
|
dup.current = dup.after.shift
|
|
dup.go
|
|
end
|
|
|
|
def go
|
|
if current then current.write_recognition(self)
|
|
else self.finish
|
|
end
|
|
end
|
|
|
|
def result(key, expression, delay = false)
|
|
unless delay
|
|
line "#{key}_value = #{expression}"
|
|
expression = "#{key}_value"
|
|
end
|
|
results[key] = expression
|
|
end
|
|
def constant_result(key, object)
|
|
constants[key] = object
|
|
end
|
|
|
|
def finish(ensure_traversal_finished = true)
|
|
pairs = []
|
|
(results.keys + constants.keys).uniq.each do |key|
|
|
pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
|
|
end
|
|
hash_expr = "{#{pairs.join(', ')}}"
|
|
|
|
statement = finish_statement.call(hash_expr)
|
|
if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
|
|
else self << statement
|
|
end
|
|
end
|
|
end
|
|
|
|
class GenerationGenerator < CodeGenerator #:nodoc:
|
|
Attributes = [:after, :before, :current, :segments]
|
|
attr_accessor(*Attributes)
|
|
FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
|
|
|
|
def initialize(*args)
|
|
super(*args)
|
|
@after, @before = [], []
|
|
@current = nil
|
|
@segments = []
|
|
end
|
|
|
|
def hash_name() 'hash' end
|
|
def local_name(key) "#{key}_value" end
|
|
|
|
def hash_value(key, assign = true, default = nil)
|
|
if locals.include?(local_name(key)) then code = local_name(key)
|
|
else
|
|
code = "hash[#{key.to_sym.inspect}]"
|
|
if assign
|
|
code = "(#{local_name(key)} = #{code})"
|
|
locals << local_name(key)
|
|
end
|
|
end
|
|
code = "(#{code} || (#{default.inspect}))" if default
|
|
return code
|
|
end
|
|
|
|
def expire_for_keys(*keys)
|
|
return if keys.empty?
|
|
conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
|
|
line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
|
|
end
|
|
|
|
def add_segment(*segments)
|
|
d = dup
|
|
d.segments.concat segments
|
|
yield d
|
|
end
|
|
|
|
def go
|
|
if current then current.write_generation(self)
|
|
else self.finish
|
|
end
|
|
end
|
|
|
|
def continue
|
|
d = dup
|
|
d.before << d.current
|
|
d.current = d.after.shift
|
|
d.go
|
|
end
|
|
|
|
def finish
|
|
line %("/#{segments.join('/')}")
|
|
end
|
|
|
|
def check_conditions(conditions)
|
|
tests = []
|
|
generator = nil
|
|
conditions.each do |key, condition|
|
|
tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
|
|
tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
|
|
generator = self.dup unless generator
|
|
end
|
|
return tests.join(' && ')
|
|
end
|
|
end
|
|
end
|
|
end
|