require 'frank/tilt' require 'frank/template_helpers' require 'frank/rescue' require 'frank/middleware/statik' require 'frank/middleware/imager' require 'frank/middleware/refresh' module Frank VERSION = '0.3.1' module Render; end class Base include Rack::Utils include Frank::Rescue include Frank::TemplateHelpers include Frank::Render attr_accessor :environment attr_accessor :proj_dir attr_accessor :server attr_accessor :static_folder attr_accessor :dynamic_folder attr_accessor :layouts_folder attr_accessor :templates def initialize(&block) instance_eval &block end def call(env) dup.call!(env) end def call!(env) @env = env @request = Rack::Request.new(env) @response = Rack::Response.new process @response.close @response.finish end private # setter for options def set(option, value) if respond_to?("#{option}=") send "#{option}=", value end end # attempt to render with the request path, # if it cannot be found, render error page def process load_helpers @response['Content-Type'] = Rack::Mime.mime_type(File.extname(@request.path), 'text/html') @response.write render(@request.path) rescue Frank::TemplateError render_404 rescue Exception => e render_500 e end # prints requests and errors to STDOUT def log_request(status, excp=nil) out = "\033[1m[#{Time.now.strftime('%Y-%m-%d %H:%M')}]\033[22m (#{@request.request_method}) http://#{@request.host}:#{@request.port}#{@request.fullpath} - #{status}" out << "\n\n#{excp.message}\n\n#{excp.backtrace.join("\n")} " if excp puts out end def load_helpers helpers = File.join(@proj_dir, 'helpers.rb') if File.exist? helpers load helpers Frank::TemplateHelpers.class_eval("include FrankHelpers") end end end module Render TMPL_EXTS = { :html => %w[haml erb rhtml builder liquid mustache textile md mkd markdown], :css => %w[sass less scss] } LAYOUT_EXTS = %w[.haml .erb .rhtml .liquid .mustache] # render request path or template path def render(path) # normalize the path path.sub!(/^\/?(.*)$/, '/\1') path.sub!(/\/$/, '/index.html') path.sub!(/(\/[\w-]+)$/, '\1.html') path = to_file_path(path) if defined? @request or path.match(/\/_[^\/]+$/) # regex for kinds that don't support meta # and define the meta delimiter nometa, delimiter = /\/_|\.(sass|less)$/, /^META-{3,}\s*$|^-{3,}META\s*$/ # set the layout layout = path.match(nometa) ? nil : layout_for(path) template_path = File.join(@proj_dir, @dynamic_folder, path) raise Frank::TemplateError, "Template not found #{template_path}" unless File.exist? template_path # read in the template # check for meta and parse it if it exists template = File.read(template_path) << "\n" ext = File.extname(path) template, meta = template.split(delimiter).reverse locals = parse_meta_and_set_locals(meta, path) # use given layout if defined as a meta field layout = locals[:layout] == 'nil' ? nil : locals[:layout] if locals.has_key?(:layout) # let tilt determine the template handler # and return some template markup if layout.nil? tilt(ext, template, locals) else layout_path = File.join(@proj_dir, @layouts_folder, layout) # add layout_path to locals raise Frank::TemplateError, "Layout not found #{layout_path}" unless File.exist? layout_path tilt(File.extname(layout), layout_path, locals) do tilt(ext, template, locals) end end end # converts a request path to a template path def to_file_path(path) file_name = File.basename(path, File.extname(path)) file_ext = File.extname(path).sub(/^\./, '') folder = File.join(@proj_dir, @dynamic_folder) engine = nil TMPL_EXTS.each do |ext, engines| if ext.to_s == file_ext engine = engines.reject do |eng| !File.exist? File.join(folder, path.sub(/\.[\w-]+$/, ".#{eng}")) end.first end end raise Frank::TemplateError, "Template not found #{path}" if engine.nil? path.sub(/\.[\w-]+$/, ".#{engine}") end # lookup the original ext for given template path # TODO: make non-ugly def ext_from_handler(extension) orig_ext = nil TMPL_EXTS.each do |ext, engines| orig_ext = ext.to_s if engines.include? extension[1..-1] end orig_ext end # reverse walks the layouts folder until we find a layout # returns nil if layout is not found def layout_for(path) layout_exts = LAYOUT_EXTS.dup ext = File.extname(path) default = 'default' << layout_ext_or_first(layout_exts, ext) file_path = path.sub(/\/[\w-]+\.[\w-]+$/, '') folders = file_path.split('/') until File.exist? File.join(@proj_dir, @layouts_folder, folders, default) break if layout_exts.empty? && folders.empty? if layout_exts.empty? layout_exts = LAYOUT_EXTS.dup default = 'default' << layout_ext_or_first(layout_exts, ext) folders.pop else default = 'default' << layout_exts.shift end end if File.exists? File.join(@proj_dir, @layouts_folder, folders, default) File.join(folders, default) else nil end end # if the given ext is a layout ext, pop it off and return it # otherwise return the first layout ext def layout_ext_or_first(layout_exts, ext) layout_exts.include?(ext) ? layout_exts.delete(ext) : layout_exts.first end # setup an object and extend it with TemplateHelpers and Render # then send everything to tilt and get some template markup back def tilt(ext, source, locals={}, &block) obj = Object.new.extend(TemplateHelpers).extend(Render) instance_variables.each do |var| unless ['@response', '@env'].include? var obj.instance_variable_set(var.intern, instance_variable_get(var)) end end Tilt[ext].new(source).render(obj, locals=locals, &block) end private # parse the given meta string with yaml # add current path # and add instance variables def parse_meta_and_set_locals(meta, path) locals = {} # parse yaml and symbolize keys if meta.nil? meta = {} else meta = YAML.load(meta).inject({}) do |options, (key, value)| options[(key.to_sym rescue key) || key] = value options end end # normalize current_path # and add it to locals current_path = path.sub(/\.[\w-]+$/, '').sub(/\/index/, '/') locals[:current_path] = current_path meta.merge(locals) end end # starts the server def self.new(&block) base = Base.new(&block) if block_given? builder = Rack::Builder.new do use Frank::Middleware::Statik, :root => base.static_folder use Frank::Middleware::Imager use Frank::Middleware::Refresh, :watch => [ base.dynamic_folder, base.static_folder, base.layouts_folder ] run base end unless base.environment == :test m = "got it under control \n got your back \n holdin' it down takin' care of business \n workin' some magic".split("\n").sort_by{rand}.first.strip puts "\n-----------------------\n" + " Frank's #{ m }...\n" + " #{base.server['hostname']}:#{base.server['port']} \n\n" server = Rack::Handler.get(base.server['handler']) server.run(builder, :Port => base.server['port'], :Host => base.server['hostname']) do trap(:INT) { puts "\n\n-----------------------\n Show's over, fellas.\n\n"; exit } end end base rescue Errno::EADDRINUSE puts " Hold on a second... Frank works alone.\n \033[31mSomething's already using port #{base.server['port']}\033[0m\n\n" end # copies over the generic project template def self.stub(project) puts "\nFrank is...\n - \033[32mCreating\033[0m your project '#{project}'" Dir.mkdir project puts " - \033[32mCopying\033[0m Frank template" FileUtils.cp_r( Dir.glob(File.join(LIBDIR, 'template/*')), project ) puts "\n \033[32mCongratulations, '#{project}' is ready to go!\033[0m" rescue Errno::EEXIST puts "\n \033[31muh oh, directory '#{project}' already exists...\033[0m" exit end end