├── .gitignore
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── brochure.gemspec
├── config.ru.example
├── lib
├── brochure.rb
└── brochure
│ ├── application.rb
│ ├── context.rb
│ ├── errors.rb
│ ├── failsafe.rb
│ ├── static.rb
│ └── template.rb
└── test
├── fixtures
├── custom404
│ └── templates
│ │ └── 404.html.erb
└── default
│ ├── helpers
│ └── link_helper.rb
│ ├── public
│ └── screen.css
│ ├── templates
│ ├── _layout.html.erb
│ ├── blog.html.erb
│ ├── blog
│ │ └── 2010.html.erb
│ ├── doctype.html.haml
│ ├── engineless.html
│ ├── error.html.erb
│ ├── haml_with_layout.html.haml
│ ├── hello.html.str
│ ├── hello.js.erb
│ ├── help
│ │ ├── index.html.erb
│ │ ├── partial_error.html.erb
│ │ └── search.html.erb
│ ├── index.html.erb
│ ├── shared
│ │ └── _head.html.erb
│ └── signup.html.erb
│ └── vendor
│ └── plugins
│ └── common
│ └── templates
│ ├── common.html.erb
│ └── shared
│ └── _footer.html.erb
└── test_brochure.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source :rubygems
2 | gemspec
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010 Sam Stephenson
2 | Copyright (c) 2010 Josh Peek
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
3 |
4 | Brochure is a Rack application for serving static sites with ERB
5 | templates (or any of the many [template languages supported by
6 | Tilt](http://github.com/rtomayko/tilt/blob/master/TEMPLATES.md#readme)).
7 | It's the good parts of PHP wrapped up in a little Ruby package —
8 | perfect for serving the marketing site for your Rails app.
9 |
10 |
11 | Sample application structure:
12 |
13 | templates/
14 | help/
15 | index.html.erb
16 | index.html.erb
17 | shared/
18 | _header.html.erb
19 | _footer.html.erb
20 | signup.html.erb
21 | config.ru
22 | public/
23 | ...
24 |
25 | Sample `config.ru`:
26 |
27 | require "brochure"
28 | root = File.dirname(__FILE__)
29 | run Brochure.app(root)
30 |
31 |
32 | ## Automatic URL mapping
33 |
34 | URLs are automatically mapped to template names:
35 |
36 | * `/` → `templates/index.html.erb`
37 | * `/signup` → `templates/signup.html.erb`
38 | * `/help/` → `templates/help/index.html.erb`
39 |
40 |
41 | ## Partials and helpers
42 |
43 | Templates can render partials. A partial is denoted by a leading
44 | underscore in its filename. So `<%= render "shared/header" %>` will
45 | render `templates/shared/_header.html.erb` inline.
46 |
47 | Partials can `<%= yield %>` back to the templates that render
48 | them. You can use this technique to extract common header and footer
49 | markup into a single layout file, for example.
50 |
51 | Templates have access to the Rack environment via the `env` method and
52 | to the Brochure application via the `application`
53 | method. Additionally, a `Rack::Request` wrapper around the Rack
54 | environment is available via the `request` method.
55 |
56 | You can print HTML-escaped strings in your templates with the `h`
57 | helper.
58 |
59 |
60 | ## Custom helper methods and instance variables
61 |
62 | You can make additional helper methods and instance variables
63 | available to your templates. Helper methods live in Ruby modules and
64 | can be included with the `:helpers` option to `Brochure.app`:
65 |
66 | module AssetHelper
67 | def asset_path(filename)
68 | local_path = File.join(application.asset_root, filename)
69 | digest = Digest::MD5.hexdigest(IO.read(local_path))
70 | "/#{filename}?#{digest}"
71 | end
72 | end
73 |
74 | run Brochure.app(root, :helpers => [AssetHelper])
75 |
76 | Similarly, instance variables can be defined with the `:assigns`
77 | option:
78 |
79 | run Brochure.app(root, :assigns => { :domain => "37signals.com" })
80 |
81 |
82 | ## Tilt template options
83 |
84 | You can specify global [Tilt template
85 | options](https://github.com/rtomayko/tilt/blob/master/TEMPLATES.md#readme)
86 | on a per-engine basis with `:template_options`:
87 |
88 | run Brochure.app(root, :template_options => {
89 | ".haml" => { :format => :html5 },
90 | ".md" => { :smart => true }
91 | })
92 |
93 |
94 | # Installation
95 |
96 | $ gem install brochure
97 |
98 | Requires [Hike](http://github.com/sstephenson/hike),
99 | [Rack](http://rack.rubyforge.org/), and
100 | [Tilt](http://github.com/rtomayko/tilt).
101 |
102 |
103 | # License
104 |
105 | Copyright (c) 2010 Sam Stephenson and Josh Peek.
106 |
107 | Released under the MIT license. See `LICENSE` for details.
108 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "rake/testtask"
2 |
3 | task :default => :test
4 |
5 | Rake::TestTask.new do |t|
6 | t.verbose = true
7 | end
8 |
--------------------------------------------------------------------------------
/brochure.gemspec:
--------------------------------------------------------------------------------
1 | spec = Gem::Specification.new do |s|
2 | s.name = "brochure"
3 | s.version = "0.5.4"
4 | s.platform = Gem::Platform::RUBY
5 | s.authors = ["Sam Stephenson", "Josh Peek"]
6 | s.email = ["sstephenson@gmail.com", "josh@joshpeek.com"]
7 | s.homepage = "http://github.com/sstephenson/brochure"
8 | s.summary = "Rack + ERB static sites"
9 | s.description = "A Rack application for serving static sites with ERB templates."
10 | s.files = Dir["lib/**/*.rb", "README.md", "LICENSE"]
11 | s.require_path = "lib"
12 |
13 | s.add_dependency "hike", "~> 1.0"
14 | s.add_dependency "rack", "~> 1.0"
15 | s.add_dependency "tilt", "~> 1.1"
16 |
17 | s.add_development_dependency "rake"
18 | s.add_development_dependency "rack-test"
19 | s.add_development_dependency "haml"
20 | end
21 |
--------------------------------------------------------------------------------
/config.ru.example:
--------------------------------------------------------------------------------
1 | require "brochure"
2 | run Brochure.app(File.dirname(__FILE__))
3 |
--------------------------------------------------------------------------------
/lib/brochure.rb:
--------------------------------------------------------------------------------
1 | require "hike"
2 | require "rack"
3 | require "tilt"
4 |
5 | module Brochure
6 | VERSION = "0.5.4"
7 |
8 | autoload :Application, "brochure/application"
9 | autoload :CaptureNotSupported, "brochure/errors"
10 | autoload :Context, "brochure/context"
11 | autoload :Failsafe, "brochure/failsafe"
12 | autoload :Static, "brochure/static"
13 | autoload :Template, "brochure/template"
14 | autoload :TemplateNotFound, "brochure/errors"
15 |
16 | def self.app(root, options = {})
17 | app = Application.new(root, options)
18 | yield app if block_given?
19 | app = Static.new(app, app.asset_root)
20 |
21 | if development?
22 | app = Rack::ShowExceptions.new(app)
23 | else
24 | app = Failsafe.new(app)
25 | end
26 |
27 | app
28 | end
29 |
30 | def self.development?
31 | ENV["RACK_ENV"] == "development"
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/brochure/application.rb:
--------------------------------------------------------------------------------
1 | module Brochure
2 | class Application
3 | attr_reader :app_root, :template_root, :asset_root, :plugin_root, :assigns, :template_options
4 |
5 | def initialize(root, options = {})
6 | @app_root = File.expand_path(root)
7 | @template_root = File.join(@app_root, "templates")
8 | @asset_root = File.join(@app_root, "public")
9 | @plugin_root = File.join(@app_root, "vendor", "plugins")
10 |
11 | @assigns = options[:assigns] || {}
12 | @template_options = options[:template_options] || {}
13 | helpers.push(*options[:helpers]) if options[:helpers]
14 | initialize_plugins
15 | end
16 |
17 | def initialize_plugins
18 | plugins.each do |plugin_root|
19 | template_trail.paths.push(File.join(plugin_root, "templates"))
20 | end
21 | end
22 |
23 | def template_trail
24 | @template_trail ||= Hike::Trail.new(app_root).tap do |trail|
25 | trail.extensions.replace(Tilt.mappings.keys.sort)
26 | trail.paths.push(template_root)
27 | end
28 | end
29 |
30 | def context_class
31 | @context_class ||= Context.for(helpers)
32 | end
33 |
34 | def templates
35 | @templates ||= {}
36 | end
37 |
38 | def helpers
39 | @helpers ||= []
40 | end
41 |
42 | def plugins
43 | @plugins ||= Dir[File.join(plugin_root, "*")].select do |entry|
44 | File.directory?(entry)
45 | end
46 | end
47 |
48 | def call(env)
49 | if forbidden?(env["PATH_INFO"])
50 | forbidden
51 | elsif template = find_template(env["PATH_INFO"])
52 | success template.render(env), template.content_type
53 | else
54 | not_found(env)
55 | end
56 | end
57 |
58 | def forbidden?(path)
59 | path[".."] || File.basename(path)[/^_/]
60 | end
61 |
62 | def find_template(logical_path, format_extension = ".html")
63 | if template_path = find_template_path(logical_path, format_extension)
64 | template_for(template_path)
65 | end
66 | end
67 |
68 | def find_partial(logical_path, format_extension = ".html")
69 | if template_path = find_partial_path(logical_path, format_extension)
70 | template_for(template_path)
71 | end
72 | end
73 |
74 | def find_template_path(logical_path, format_extension)
75 | template_trail.find(
76 | logical_path,
77 | logical_path + format_extension,
78 | File.join(logical_path, "index" + format_extension)
79 | )
80 | end
81 |
82 | def find_partial_path(logical_path, format_extension)
83 | dirname, basename = File.split(logical_path)
84 | if dirname == "."
85 | partial_path = "_" + basename
86 | else
87 | partial_path = File.join(dirname, "_" + basename)
88 | end
89 | template_trail.find(partial_path, partial_path + format_extension)
90 | end
91 |
92 | def template_for(template_path)
93 | if Brochure.development?
94 | Template.new(self, template_path)
95 | else
96 | templates[template_path] ||= Template.new(self, template_path)
97 | end
98 | end
99 |
100 | def context_for(template, env)
101 | context_class.new(self, template, env, assigns)
102 | end
103 |
104 | def respond_with(status, body, content_type = "text/html; charset=utf-8")
105 | headers = {
106 | "Content-Type" => content_type,
107 | "Content-Length" => Rack::Utils.bytesize(body).to_s
108 | }
109 | [status, headers, [body]]
110 | end
111 |
112 | def success(body, content_type)
113 | respond_with 200, body, content_type
114 | end
115 |
116 | def not_found(env)
117 | if template = find_template("404")
118 | respond_with 404, template.render(env)
119 | else
120 | default_not_found
121 | end
122 | end
123 |
124 | def default_not_found
125 | respond_with 404, <<-HTML
126 |
127 |
Hello #{request.params["name"]}
2 | Home 3 | -------------------------------------------------------------------------------- /test/fixtures/default/templates/hello.js.erb: -------------------------------------------------------------------------------- 1 | var domain = "<%= @domain %>"; 2 | -------------------------------------------------------------------------------- /test/fixtures/default/templates/help/index.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= render "shared/head", :title => "Help" %> 4 | 5 |Hello Sam
"] 113 | end 114 | 115 | def test_block_configured 116 | app do |a| 117 | path = '../custom404/templates' 118 | a.template_trail.paths.push path 119 | end 120 | get '/404' 121 | assert last_response.ok? 122 | end 123 | 124 | def test_templates_in_plugins 125 | get "/common" 126 | assert last_response.body["Common template"] 127 | end 128 | 129 | def test_rendering_partials_in_plugins 130 | get "/help" 131 | assert last_response.body["