fir 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/LICENSE +20 -0
  2. data/README.markdown +109 -0
  3. data/bin/fir +18 -0
  4. data/example/Rakefile +4 -0
  5. data/example/boot_example.rb +7 -0
  6. data/example/config.rb +8 -0
  7. data/example/config.ru +12 -0
  8. data/example/layouts/application.html.erb +34 -0
  9. data/example/menus.yml +6 -0
  10. data/example/pages.yml +15 -0
  11. data/example/pages/404.html.erb +1 -0
  12. data/example/pages/contact-us.markdown +6 -0
  13. data/example/pages/index.markdown +4 -0
  14. data/example/pages/products/dynamite-plus.html.erb +7 -0
  15. data/example/pages/products/dynamite.html.erb +7 -0
  16. data/example/pages/products/index.markdown +6 -0
  17. data/example/pages/products/mouse-trap.html.erb +7 -0
  18. data/example/public/manage-content.html +55 -0
  19. data/example/public/static.txt +1 -0
  20. data/example/public/stylesheets/style.css +8 -0
  21. data/example/tmp/restart.txt +1 -0
  22. data/example/users.yml +5 -0
  23. data/lib/fir.rb +17 -0
  24. data/lib/fir/admin.rb +150 -0
  25. data/lib/fir/boot.rb +14 -0
  26. data/lib/fir/helpers.rb +41 -0
  27. data/lib/fir/pages.rb +178 -0
  28. data/lib/fir/static.rb +45 -0
  29. data/lib/fir/tasks.rb +32 -0
  30. data/lib/fir/util.rb +52 -0
  31. data/skeleton/Rakefile +4 -0
  32. data/skeleton/config.rb +8 -0
  33. data/skeleton/config.ru +12 -0
  34. data/skeleton/layouts/application.html.erb +34 -0
  35. data/skeleton/menus.yml +5 -0
  36. data/skeleton/pages.yml +6 -0
  37. data/skeleton/pages/404.html.erb +1 -0
  38. data/skeleton/pages/contact.markdown +2 -0
  39. data/skeleton/pages/index.markdown +2 -0
  40. data/skeleton/public/stylesheets/style.css +8 -0
  41. data/skeleton/tmp/restart.txt +1 -0
  42. data/spec/fir_spec.rb +71 -0
  43. data/spec/spec_helper.rb +9 -0
  44. metadata +159 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Jarrett Colby
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Fir Isn't Rails
2
+ ===============
3
+
4
+ Fir Isn't Rails is a minimalist Ruby framework for small, static sites. It's Rack-based, so it runs on Phusion Passenger.
5
+
6
+ Warning: Alpha Release
7
+ ======================
8
+
9
+ Fir is still in alpha. It has only been lightly tested. You may very well encounter bugs. If you do, please visit Fir's GitHub repository and tell us what happened.
10
+
11
+ Installation
12
+ ============
13
+
14
+ sudo gem intall fir
15
+ fir my_fir_site
16
+
17
+ The second command runs the Fir generator, creating a skeleton Fir site in the directory `my_fir_site`.
18
+
19
+ Before we set up Passenger, let's test the site with Rackup:
20
+
21
+ cd my_fir_site
22
+ rackup config.ru -p 3000
23
+
24
+ The `-p` option tells Rackup which port to listen on. This can be anything you want. (Obviously, if you're already running Rails or anything else on port 3000, you need to give Rack a different port.)
25
+
26
+ Open your browser and visit:
27
+
28
+ http://localhost:3000
29
+
30
+ You should see the home page. If you don't, something is wrong. Please visit Fir's GitHub repository and tell us what happened.
31
+
32
+ Rackup is recommended when you're developing your site locally. Press ctrl-c to kill Rackup.
33
+
34
+ Now we're ready to make it work with Passenger, which you're encouraged to use in production. At this point, the instructions become a little iffy, because so much depends on your individual system (e.g. how Apache is configured, whether you can `sudo`). If you're on shared hosting, you'll probably need to contact support and have them perform some of these steps for you.
35
+
36
+ For now, let's assume you're on a Mac with the default Apache installation.
37
+
38
+ If you haven't done so, [install Passenger](http://www.modrails.com/documentation/Users%20guide.html). You should also read "[Deploying a Rack-based Ruby application](http://www.modrails.com/documentation/Users%20guide.html#_deploying_a_rack_based_ruby_application)" in the Passenger Users' Guide.
39
+
40
+ In `httpd.conf`, add a virtual host:
41
+
42
+ <VirtualHost *:80>
43
+ ServerName example.com
44
+ DocumentRoot /absolute/path/to/my_fir_site/public
45
+ RackBaseURI /
46
+ <Directory "/absolute/path/to/my_fir_site/public">
47
+ Options Indexes FollowSymLinks
48
+ Order Deny,Allow
49
+ Allow from all
50
+ </Directory>
51
+ </VirtualHost>
52
+
53
+ Make sure you edit the correct `httpd.conf`! Some systems may have more than one copy of Apache installed.
54
+
55
+ Check `httpd.conf` syntax and restart Apache:
56
+
57
+ sudo apachectl configtest
58
+ sudo apachectl graceful
59
+
60
+ That should do it.
61
+
62
+ Basic Usage
63
+ ===========
64
+
65
+ See the [example app](http://github.com/jarrett/fir-example) to get you started.
66
+
67
+ ## Editing the layout ##
68
+
69
+ Each page will be rendered inside `application.html.erb` in the `layouts` directory. (That name was chosen because it will no doubt be familiar to Rails developers.)
70
+
71
+ The current version does not support alternate layouts. Such a feature may be added in the future if there's a need.
72
+
73
+ ## Creating Pages ##
74
+
75
+ Pages live in the `pages` directory, or in a subdirectory thereof. If you create a page called `my-page.markdown` in `pages`, Fir will map it to this URL: `http://youdomain.com/my-page`. Similarly, if you create a subdirectory in `pages` called `subdir`, and put `my-page.markdown` in there, this URL will be used: `http://youdomain.com/subdir/my-page`.
76
+
77
+ Pages can have the following extensions:
78
+
79
+ .html
80
+ .html.erb
81
+ .markdown
82
+
83
+ Markdown pages get passed through ERB before the Markdown is transformed to HTML. Thus, you can make Markdown pages dynamic. Remember: ERB runs **before** Markdown, so whatever your ERB tags spit out will be interpreted as Markdown.
84
+
85
+ ## Configuring Pages ##
86
+
87
+ `pages.yml` contains all per-page configurations. Page titles, meta descriptions, and other configs can be specified there. Each page config property will be made available to the templates as an instance variable.
88
+
89
+ ## Mapping URLs ##
90
+
91
+ You can override the default URL mapping in `pages.yml`. Just add a `path` property to a page, and the page will be made available under the path you specify.
92
+
93
+ ## Content Management ##
94
+
95
+ Most of us will be perfectly happy managing content by editing the files in the `pages` directory. But for the sake of our clients, Fir offers another way.
96
+
97
+ Fir isn't a CMS. Instead, it exposes a minimal, RESTful API for managing content. The idea is that content should be managed with specially-designed Fir clients, rather than through Fir itself. You can see how this works by downloading the [example app](http://github.com/jarrett/fir-example), booting it, and visiting `/manage-content.html` in your browser. The source of that page is available [on GitHub](http://github.com/jarrett/fir-example/blob/master/public/manage-content.html).
98
+
99
+ Webserver Support
100
+ =================
101
+
102
+ Fir is designed primarily for Phusion Passenger. As a Rack-based framework, it theoretically supports Mongrel, CGI, FastCGI, and a host of others. But as of this writing, Fir has only been tested with Rackup and Passenger. If you get Fir working with anything else, please visit Fir's GitHub repository and let us know how you did it.
103
+
104
+ Unicode and Others
105
+ ==================
106
+
107
+ Fir doesn't change the character encoding of your content, so you can technically use any encoding scheme you want. That being said, UTF-8 is recommended. As always, make sure your HTML template correctly identifies the encoding scheme. (See [The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets](http://www.joelonsoftware.com/articles/Unicode.html) for more information.)
108
+
109
+ Don't use non-English characters in your URLs. They don't play nice with the filesystem, and they have to be percent-encoded anyway, resulting in ugly URLs. Avoid special characters other than `-` and `_`.
data/bin/fir ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift '/Users/jarrettcolby/apps/fir/lib'
4
+ require 'rubygems'
5
+ require 'fir'
6
+ require 'fileutils'
7
+ include FileUtils
8
+
9
+ unless ARGV.length >= 1
10
+ raise 'Usage: fir path'
11
+ end
12
+
13
+ fir_root = ARGV[0]
14
+ if File.exists?(fir_root)
15
+ raise "#{fir_root} already exists! Aborting."
16
+ end
17
+ cp_r FIR_SKELETON_ROOT, fir_root
18
+ puts "Created new Fir site in #{fir_root}"
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'fir'
3
+
4
+ ::Fir::Tasks.define
@@ -0,0 +1,7 @@
1
+ # This file is NOT normally part of a Fir deployment. It only exists so that we can
2
+ # run the example app against the Fir code in this particular gem distribution,
3
+ # instead of against whatever version of the Fir gem (if any) happens to be installed
4
+ # on the local system.
5
+
6
+ lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
7
+ puts `rackup config.ru -p 3001 -e "$:.unshift '#{lib_dir}'"`
@@ -0,0 +1,8 @@
1
+ # This is the main config file for your site. Add any "require" statements or any other global directives here.
2
+
3
+ require 'maruku' # Comment out if you don't want to use Markdown
4
+
5
+ Fir.config do |config|
6
+ config.site_name = 'Acme Co.'
7
+ config.perform_caching = false
8
+ end
@@ -0,0 +1,12 @@
1
+ # This file tells Rack how to boot Fir. It's the point of entry for the whole framework.
2
+ # You shouldn't have to modify this file. If you came here looking for the app's config
3
+ # file, you're in the wrong place. Look at config.rb instead.
4
+
5
+ FIR_ROOT = File.expand_path(File.dirname(__FILE__))
6
+
7
+ require 'fir.rb'
8
+ require 'config.rb'
9
+
10
+ # This is where it all begins.
11
+ # See boot.rb in the Fir gem if you're curious about what happens next.
12
+ instance_eval(&Fir.boot_proc)
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml">
4
+
5
+ <head>
6
+ <title>
7
+ <% if @title and !@title.empty? %>
8
+ <%= h @title %> -
9
+ <% end %>
10
+ Acme Co.
11
+ </title>
12
+ <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
13
+ <% if @description and !@description.empty? %>
14
+ <meta name="description" content="<%= h @page_description %>"/>
15
+ <% end %>
16
+ <link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
17
+ </head>
18
+
19
+ <body>
20
+
21
+ <div id="container">
22
+ <ul id="top_nav">
23
+ <% menu('top') do |item, current_page| %>
24
+ <!-- current_page is true if the menu item links to the current page, false otherwise.
25
+ It's usually not necessary, since you'll automatically get either an <a> or <span class="selected">.-->
26
+ <li><%= item %></li>
27
+ <% end %>
28
+ </ul>
29
+
30
+ <%= yield %>
31
+ </div>
32
+
33
+ </body>
34
+ </html>
@@ -0,0 +1,6 @@
1
+ # You must restart your server after editing this file.
2
+
3
+ top:
4
+ - Home:
5
+ - Products: products
6
+ - Contact Us: contact-us
@@ -0,0 +1,15 @@
1
+ # You must restart your server after editing this file.
2
+
3
+ index:
4
+ title: Home
5
+ description: Acme Co. sells all the widgets you need to capture road runners and other desert fauna
6
+ products/index:
7
+ title: Products
8
+ description: Acme Company's products
9
+ products/dynamite:
10
+ title: Dynamite
11
+ products/dynamite-plus:
12
+ title: Dynamite
13
+ path: products/dynamite+ # Override the default user-visible path. Note that you need to percent-encode the + in URLs.
14
+ products/mouse-trap:
15
+ title: Mouse Trap
@@ -0,0 +1 @@
1
+ <h1>Sorry, there is nothing at this address. (404 Not Found)</h1>
@@ -0,0 +1,6 @@
1
+ Contact Us
2
+ ==========
3
+
4
+ 555-123-4567
5
+ 98765 Runner Road
6
+ Coyote, AZ
@@ -0,0 +1,4 @@
1
+ Home
2
+ ====
3
+
4
+ Welcome to Acme Company!
@@ -0,0 +1,7 @@
1
+ <h1>Dynamite Plus!</h1>
2
+
3
+ <p><%= link_to 'Back to products', '/products' %></p>
4
+
5
+ <p>Like our popular <%= link_to 'Dynamite', '/products/dynamite' %>, but even more powerful!</p>
6
+
7
+ <p>Released: Feb 1st 1956 (<%= Date.today.year - 1956 %> years ago)</p>
@@ -0,0 +1,7 @@
1
+ <h1>Dynamite</h1>
2
+
3
+ <p><%= link_to 'Back to products', '/products' %></p>
4
+
5
+ <p>Acme's most popular product! Suitable for all forms of demolition and hunting road runners.</p>
6
+
7
+ <p>Released: Jan 19th 1956 (<%= Date.today.year - 1956 %> years ago)</p>
@@ -0,0 +1,6 @@
1
+ Products
2
+ ========
3
+
4
+ - [Dynamite](/products/dynamite)
5
+ - [Dynamite Plus!](/products/dynamite%2B)
6
+ - [Mouse Trap](/products/mouse-trap)
@@ -0,0 +1,7 @@
1
+ <h1>Mouse Trap</h1>
2
+
3
+ <p><%= link_to 'Back to products', '/products' %></p>
4
+
5
+ <p>Catch those varmints!</p>
6
+
7
+ <p>Released: May 12th 1964 (<%= Date.today.year - 1964 %> years ago)</p>
@@ -0,0 +1,55 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml">
4
+
5
+ <head>
6
+ <title>Content Management API</title>
7
+ <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
8
+ <link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
9
+ <style type="text/css">
10
+ form { border: 2px solid #a0a0a0; padding: 8px; background: #f0f0f0; }
11
+ </style>
12
+ </head>
13
+
14
+ <body>
15
+
16
+ <div id="container">
17
+
18
+ <h1>Content Management API</h1>
19
+
20
+ <p>This page exists to demonstrate Fir's content management API. It would normally not be included in a Fir site. Instead, you would use a more user-friendly Fir client.</p>
21
+
22
+ <p>Fir exposes a simple RESTful API. It uses HTTP basic authentication. The credentials for this site, as defined in <code>users.yml</code>, are:</p>
23
+
24
+ <p><pre>username: johndoe
25
+ password: foo
26
+ </pre></p>
27
+
28
+ <p>This is why FIR isn't really a CMS&mdash;it doesn't provide a user-friendly content management interface. Instead, it exposes a machine-friendly API that's meant to be accessed by remote clients. Anyone can build a Fir client by following the examples on this page.</p>
29
+
30
+ <h2>Listing Pages</h2>
31
+
32
+ <p>An XML feed of pages is available at:</p>
33
+ <p><code><a href="/-admin/pages">/-admin/pages</a></code></p>
34
+
35
+ <h2>Downloading Pages</h2>
36
+
37
+ <p>Download the source code of individual pages from URLs like this:</p>
38
+ <p><code><a href="/-admin/pages/index.markdown">/-admin/pages/index.markdown</a></code></p>
39
+ <p><code><a href="/-admin/pages/products/dynamite.html.erb">/-admin/pages/products/dynamite.html.erb</a></code></p>
40
+
41
+ <h2>Updating Pages</h2>
42
+
43
+ <p>Update page source code by POSTing to the same URLs that are used to download the pages:</p>
44
+
45
+ <form action="/-admin/pages/index.markdown" method="post">
46
+ <p>This form POSTs to:<br/><code>/-admin/pages/index.markdown</code></p>
47
+
48
+ <p><textarea name="content" rows="5" cols="80">Whatever you enter here will replace the existing content of index.markdown. This textarea's name is "content."</textarea></p>
49
+
50
+ <p><input type="submit" value="Submit"/></p>
51
+ </form>
52
+
53
+ </div><!-- container -->
54
+ </body>
55
+ </html>
@@ -0,0 +1 @@
1
+ This is static.txt. It exists to prove that you can access static files in the public folder.
@@ -0,0 +1,8 @@
1
+ body { font-family: helvetica, arial, verdana, sans-serif; font-size: 83%; text-align: center; }
2
+ /* General typography */
3
+ h1 { font-size: 2em; }
4
+ h2 { font-size: 1.7em; }
5
+ h3 { font-size: 1.5em; }
6
+ /* Layout elements */
7
+ div#container { width: 960px; margin-left: auto; margin-right: auto; text-align: left; }
8
+ /* Specific pages */
@@ -0,0 +1 @@
1
+ Run "touch tmp/restart.txt" to restart Passenger. (This doesn't work if you stared Fir using rackup.)
@@ -0,0 +1,5 @@
1
+ ---
2
+ johndoe:
3
+ salt: b64e9a6e7e2c187a81a30de2d0
4
+ # Password is "foo"
5
+ crypted_password: f8463d3c8d229f4daec934d23db3b6f1d5b5eedc56a965c6721ed8b697e60a7d4dfe7a9e249eb3fc02d00c9528852b20bda276ff6ec05e972e7f5f338b1aa59e
@@ -0,0 +1,17 @@
1
+ require 'rack'
2
+ require 'ostruct'
3
+ require 'yaml'
4
+ require 'digest'
5
+ require 'erb'
6
+ require 'builder'
7
+ require 'maruku'
8
+
9
+ require 'fir/admin.rb'
10
+ require 'fir/boot.rb'
11
+ require 'fir/helpers.rb'
12
+ require 'fir/pages.rb'
13
+ require 'fir/static.rb'
14
+ require 'fir/tasks.rb'
15
+ require 'fir/util.rb'
16
+
17
+ FIR_SKELETON_ROOT = File.join(File.expand_path(File.dirname(__FILE__)), '..', 'skeleton')
@@ -0,0 +1,150 @@
1
+ module Fir
2
+ module AdminMiddleware
3
+ def admin_path?(env)
4
+ !!env['PATH_INFO'].chomp('/').match(/^\/?-admin/)
5
+ end
6
+ end
7
+
8
+ class AdminAuth < ::Rack::Auth::Basic
9
+ include AdminMiddleware
10
+
11
+ def initialize(*args)
12
+ super
13
+ if File.exists?('users.yml')
14
+ @users = YAML::load(File.read('users.yml'))
15
+ @users = {} unless @users.is_a?(Hash) # In case of an invalid YAML file
16
+ else
17
+ @users = {}
18
+ end
19
+ @authenticator = lambda do |username, password|
20
+ if @users[username]
21
+ salt = @users[username]['salt']
22
+ expected = @users[username]['crypted_password']
23
+ actual = Fir.encrypt_password(password, salt)
24
+ expected == actual
25
+ else
26
+ false
27
+ end
28
+ end
29
+ end
30
+
31
+ def call(env)
32
+ if admin_path?(env)
33
+ super(env)
34
+ else
35
+ @app.call(env)
36
+ end
37
+ end
38
+ end
39
+
40
+ class AdminInterface
41
+ include AdminMiddleware
42
+
43
+ def initialize(app)
44
+ @app = app
45
+ @file_server = ::Rack::File.new(File.join(FIR_ROOT, 'public'))
46
+ end
47
+
48
+ def call(env)
49
+ session = env['rack.session']
50
+ path = env['PATH_INFO'].chomp('/')
51
+ if admin_path?(env)
52
+ match = path.match(/^\/?-admin\/pages\/(.+)/)
53
+ if match
54
+ page = Fir.sanitize_page(match[1])
55
+ page_path = File.join(FIR_ROOT, 'pages', page)
56
+ if File.exists?(page_path)
57
+ case env['REQUEST_METHOD'].downcase
58
+ when 'get'
59
+ retrieve_page(page_path)
60
+ when 'post'
61
+ update_page(page_path, env)
62
+ else
63
+ return method_not_allowed('Only GET and POST are allowed')
64
+ end
65
+ else
66
+ not_found
67
+ end
68
+ elsif path.match(/^\/?-admin\/pages$/)
69
+ page_index
70
+ else
71
+ not_found
72
+ end
73
+ else
74
+ return @app.call(env)
75
+ end
76
+ end
77
+
78
+ protected
79
+
80
+ def page_index
81
+ builder = Builder::XmlMarkup.new(:indent => 2)
82
+ builder.instruct!
83
+ xml = builder.pages do |pages|
84
+ recurse = lambda do |dir, node|
85
+ dir.each do |entry|
86
+ if entry[0].chr != '.'
87
+ path = File.join(dir.path, entry)
88
+ if File.file?(path)
89
+ node.page do |page|
90
+ page.name entry
91
+ end
92
+ else # Directory
93
+ node.folder do |folder|
94
+ folder.name entry
95
+ folder.pages do |folder_pages|
96
+ recurse.call(Dir.new(path), folder_pages)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ recurse.call(Dir.new('pages'), pages)
104
+ end
105
+ [200, {'Content-Type' => 'text/xml'}, xml]
106
+ end
107
+
108
+ def retrieve_page(page_path)
109
+ if File.readable?(page_path)
110
+ source = File.read(page_path)
111
+ directory, filename, ext = Fir.split_path(page_path)
112
+ filename_with_ext = filename + '.' + ext
113
+ [200, {'Content-Type' => 'text/plain', 'Content-Disposition' => "attachment; #{filename_with_ext}"}, source]
114
+ else
115
+ internal_server_error('File exists on disk, but is not readable. Are the permissions wrong?')
116
+ end
117
+ end
118
+
119
+ def update_page(page_path, env)
120
+ if File.writable?(page_path)
121
+ File.open(page_path, 'w') do |file|
122
+ request = ::Rack::Request.new(env)
123
+ file.write(request.POST['content'])
124
+ end
125
+ Fir.clear_cache(page_path)
126
+ [200, {'Content-Type' => 'text/plain'}, "#{page_path} has been saved."]
127
+ else
128
+ internal_server_error('File exists on disk, but is not writeable. Are the permissions wrong?')
129
+ end
130
+ end
131
+
132
+ ### HTTP Response Codes ###
133
+
134
+ def bad_request(explanation = nil)
135
+ [400, {'Content-Type' => 'text/plain'}, "400 Bad Request #{explanation}" ]
136
+ end
137
+
138
+ def method_not_allowed(explanation = nil)
139
+ [405, {'Content-Type' => 'text/plain'}, "405 Method Not Allowed #{explanation}" ]
140
+ end
141
+
142
+ def not_found(explanation = nil)
143
+ [404, {'Content-Type' => 'text/plain'}, "404 Not Found #{explanation}" ]
144
+ end
145
+
146
+ def internal_server_error(explanation = nil)
147
+ [500, {'Content-Type' => 'text/plain'}, "500 Internal Server Error #{explanation}" ]
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,14 @@
1
+ module Fir
2
+ def self.boot_proc
3
+ # This get called in config.ru. Thus, Passenger can eval the contents of config.ru the way it wants to
4
+ lambda do
5
+ ::Fir.validate_config
6
+ #use ::Rack::Reloader # Only when developing the Fir gem
7
+ use ::Rack::ContentLength
8
+ use ::Fir::Static
9
+ use ::Fir::AdminAuth
10
+ use ::Fir::AdminInterface
11
+ run ::Fir::PageRenderer.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ module Fir
2
+ module Helpers
3
+ include ::ERB::Util
4
+
5
+ def absolutize(path)
6
+ File.join(Fir.config.relative_url_root, path)
7
+ end
8
+
9
+ def content_tag(name, content_or_options = {}, options = {})
10
+ if content_or_options.is_a?(Hash)
11
+ options = content_or_options
12
+ content = nil
13
+ else
14
+ content = content_or_options
15
+ end
16
+ Builder::XmlMarkup.new.tag!(name, content, options)
17
+ end
18
+
19
+ def link_to(text, url)
20
+ content_tag('a', text, 'href' => url)
21
+ end
22
+
23
+ class MenuNotDefined < StandardError; end
24
+ def menu(name, options = {})
25
+ raise(ArgumentError, 'menu requires a block') unless block_given?
26
+ options = {:link_to_current => false}.merge(options)
27
+ items = @menus[name] || raise(MenuNotDefined, "Menu '#{name}' is not defined. Define it in menus.yml.")
28
+ items.each do |pair|
29
+ # pair looks like: {'Menu Text' => 'path' } or {'Menu Text' => {'link' => 'path', 'other_option' => 'value'}}
30
+ text = pair.keys.first
31
+ item_options = pair[text]
32
+ path = absolutize((item_options.is_a?(Hash) ? item_options['link'] : item_options) || '')
33
+ if options[:link_to_current] or path != @request.path_info
34
+ yield link_to(text, path), true
35
+ else
36
+ yield content_tag('span', text, 'class' => 'selected'), false
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,178 @@
1
+ module Fir
2
+ module TemplateLoading
3
+ protected
4
+
5
+ def load_template(path, base_dir = 'pages')
6
+ directory, filename, ext = Fir.split_path(path)
7
+ path_without_extension = File.join(FIR_ROOT, base_dir, directory, filename)
8
+ path_and_extension = template_extensions(base_dir).collect do |ext|
9
+ [path_without_extension + '.' + ext, ext]
10
+ end.detect do |full_path, ext|
11
+ File.exists?(full_path)
12
+ end
13
+ return nil if path_and_extension.nil?
14
+ full_path = path_and_extension[0]
15
+ extension = path_and_extension[1]
16
+ template_formats[base_dir][extension].new(full_path)
17
+ end
18
+
19
+ def template_extensions(base_dir)
20
+ template_formats[base_dir].keys
21
+ end
22
+
23
+ # Must return a hash with the following structure. The top-level keys map to directories, e.g. pages and layouts.
24
+ # The second-level keys are file extensions, and the second-level values are subclasses up TemplateAdapter.
25
+ def template_formats
26
+ {
27
+ 'pages' => {'html.erb' => ErbAdapter, 'html' => NoOpAdapter, 'markdown' => MarkdownAdapter},
28
+ 'layouts' => {'html.erb' => ErbAdapter}
29
+ }
30
+ end
31
+ end
32
+
33
+ class PageRenderer
34
+ include TemplateLoading
35
+
36
+ def initialize
37
+ @page_config = yaml_or_empty('pages.yml')
38
+ @path_mappings = @page_config.inject({}) do |mappings, (page, configs)|
39
+ mappings[configs['path']] = page if configs['path']
40
+ mappings
41
+ end
42
+ @menus = yaml_or_empty('menus.yml')
43
+ end
44
+
45
+ def call(env)
46
+ status = 200
47
+ content_type = 'text/html'
48
+ body = ''
49
+ path = ::Rack::Utils.unescape(env['PATH_INFO']).chomp('/').gsub(/^\//, '')
50
+ path = Fir.sanitize_page(path) # path now looks like 'folder/page-name'
51
+ if @path_mappings.has_key?(path)
52
+ path = @path_mappings[path]
53
+ end
54
+ template_options = {:menus => @menus, :request => Rack::Request.new(env)}
55
+ if directory?(path)
56
+ # This is like Apache's DirectoryIndex: if the request is for
57
+ # a directory, look for a page called "index" in that directory.
58
+ path = File.join(path, 'index').gsub(/^\//, '') # path now looks like 'folder/index' or just 'index'
59
+ end
60
+ if template = load_template(path)
61
+ body = template.render_with_layout(template_options.merge(:page_config => page_config(path)))
62
+ if Fir.config.perform_caching
63
+ cache_result(path, body)
64
+ end
65
+ elsif template = load_template('404')
66
+ # Page not found. Render 404 template.
67
+ status = 404
68
+ body = template.render_with_layout(template_options)
69
+ else
70
+ # Page not found, and neither was the 404 template! Render a simple message.
71
+ content_type = 'text/plain'
72
+ status = 404
73
+ body = 'Sorry, there is nothing at this address. (404 Not Found)'
74
+ end
75
+ [status, {'Content-Type' => content_type}, body ]
76
+ end
77
+
78
+ protected
79
+
80
+ def cache_result(path, body)
81
+ directory, filename, ext = Fir.split_path(path)
82
+ path = File.join(FIR_ROOT, 'public/cache', directory, filename + '.html')
83
+ File.open(path, 'w') do |file|
84
+ file.write(body)
85
+ end
86
+ end
87
+
88
+ def directory?(path)
89
+ File.directory?(File.join('pages', path))
90
+ end
91
+
92
+ def page_config(path)
93
+ @page_config[path] || {}
94
+ end
95
+
96
+ # Load the YAML file, or return an empty hash if the file can't be loaded
97
+ def yaml_or_empty(file)
98
+ (File.exists?(file) and File.readable?(file)) ?
99
+ YAML::load(File.read(file)) || {} : {}
100
+ end
101
+ end
102
+
103
+ class TemplateAdapter
104
+ include TemplateLoading
105
+
106
+ def initialize(template_path)
107
+ # ERB uses the template adapter's bindings, so we don't want to pollute the
108
+ # namespace any more than we have too. Thus the two preceding underscores.
109
+ @__path = template_path
110
+ end
111
+
112
+ def render_with_layout(options)
113
+ wrapper_template.render(options) do
114
+ # This block will be called where "yield" appears in the wrapper template.
115
+ # See comment on ErbAdapter#render.
116
+ render(options)
117
+ end
118
+ end
119
+
120
+ protected
121
+
122
+ # Returns an instance of ErbAdapter
123
+ def wrapper_template
124
+ load_template('application', 'layouts') || raise('layouts/application.html.erb must exist!')
125
+ end
126
+
127
+ def source
128
+ File.read(@__path)
129
+ end
130
+ end
131
+
132
+ # Passes through the content as-is.
133
+ # This is desirable for, e.g., .html files.
134
+ class NoOpAdapter < TemplateAdapter
135
+ def render(options)
136
+ source
137
+ end
138
+ end
139
+
140
+ class ErbAdapter < TemplateAdapter
141
+ include ::Fir::Helpers
142
+
143
+ # If a block is given, it will be included in ERB's binding. Thus, if the template yields,
144
+ # the block given to render will be called.
145
+ def render(options, &block)
146
+ render_erb(source, options, &block)
147
+ end
148
+
149
+ protected
150
+
151
+ def render_erb(__erb_source, __options)
152
+ if __options[:page_config]
153
+ __options[:page_config].each do |key, value|
154
+ instance_variable_set('@' + key, value)
155
+ end
156
+ end
157
+ @menus = __options[:menus]
158
+ @request = __options[:request]
159
+ ERB.new(__erb_source).result(binding)
160
+ end
161
+ end
162
+
163
+ # Uses Maruku
164
+ class MarkdownAdapter < ErbAdapter
165
+ def render(options)
166
+ render_markdown(render_erb(source, options), options)
167
+ end
168
+
169
+ protected
170
+
171
+ def render_markdown(markdown_source, options)
172
+ unless defined?(Maruku)
173
+ raise 'Markdown requires Maruku. "gem install maruku" if necessary, then "require \'maruku\'" in config.rb'
174
+ end
175
+ Maruku.new(markdown_source).to_html
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,45 @@
1
+ module Fir
2
+ class Static
3
+ FORBIDDEN_DIRS = ['.svn', '.git']
4
+
5
+ def initialize(app)
6
+ @app = app
7
+ @file_server = ::Rack::File.new(File.join(FIR_ROOT, 'public'))
8
+ end
9
+
10
+ def call(env)
11
+ path = Fir.sanitize_page(::Rack::Utils.unescape(env['PATH_INFO']).chomp('/'))
12
+ method = env['REQUEST_METHOD']
13
+ if method == 'GET'
14
+ if static_exists?(path, true)
15
+ return @file_server.call(env)
16
+ elsif Fir.config.perform_caching
17
+ cached_path = File.join('cache', path)
18
+ # Change PATH_INFO because @file_server uses that variable to find the file
19
+ if static_exists?(cached_path, false) # Is it a directory?
20
+ index_path = File.join(cached_path, 'index.html')
21
+ if static_exists?(index_path, true)
22
+ env['PATH_INFO'] = index_path
23
+ return @file_server.call(env)
24
+ else # If the directory index isn't cached, pass on the request so PageRenderer can look for an index file.
25
+ return @app.call(env)
26
+ end
27
+ elsif static_exists?(cached_path + '.html', true) # Is it a file?
28
+ env['PATH_INFO'] = cached_path + '.html'
29
+ return @file_server.call(env)
30
+ end
31
+ end
32
+ end
33
+ return @app.call(env)
34
+ end
35
+
36
+ protected
37
+
38
+ # If look_for_files is true, use File.file?, else use File.directory?
39
+ def static_exists?(path, look_for_files)
40
+ return false unless (path.split('/') & FORBIDDEN_DIRS).empty?
41
+ path = File.join(@file_server.root, path)
42
+ (look_for_files ? File.file?(path) : File.directory?(path)) and File.readable?(path)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ module Fir
2
+ module Tasks
3
+ def self.define
4
+ namespace :users do
5
+ desc 'Add a user for the content management API. If the user already exists, the password is overwritten. ' +
6
+ 'You must restart your server for the change to take effect. Usage: rake add_user user=username pass=password'
7
+ task :add do
8
+ require 'fir'
9
+ require 'active_support/secure_random'
10
+ unless (ENV.has_key?('user') or ENV.has_key?('USER')) and (ENV.has_key?('pass') or ENV.has_key?('PASS'))
11
+ raise 'usage: rake users:add user=username pass=password'
12
+ end
13
+ user = ENV['user'] || ENV['USER']
14
+ pass = ENV['pass'] || ENV['PASS']
15
+
16
+ if File.exists?('users.yml')
17
+ pairs = YAML::load(File.read('users.yml')) || {}
18
+ else
19
+ pairs = {}
20
+ end
21
+
22
+ salt = ActiveSupport::SecureRandom.hex(13)
23
+ pairs[user] = {'crypted_password' => Fir.encrypt_password(pass, salt), 'salt' => salt}
24
+
25
+ File.open('users.yml', 'w') do |file|
26
+ file.write(YAML::dump(pairs))
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ module Fir
2
+ # Page is a path such as you would find in a URL, not a local filesystem path
3
+ def self.clear_cache(page)
4
+ directory, filename, ext = Fir.split_path(page)
5
+ path = File.join(FIR_ROOT, 'public/cache', directory, filename + '.html')
6
+ if File.exists?(path)
7
+ File.delete(path)
8
+ end
9
+ end
10
+
11
+ def self.config
12
+ @config ||= OpenStruct.new
13
+ yield @config if block_given?
14
+ # Default configs go here
15
+ @config.perform_caching ||= false
16
+ @config.relative_url_root ||= ''
17
+ @config
18
+ end
19
+
20
+ def self.encrypt_password(password, salt)
21
+ crypted_password = password + salt
22
+ 3.times do
23
+ crypted_password = Digest::SHA512.hexdigest(crypted_password)
24
+ end
25
+ crypted_password
26
+ end
27
+
28
+ def self.sanitize_page(page)
29
+ page.gsub('../', '')
30
+ end
31
+
32
+ # returns [directories, name w/o extension, extension]
33
+ def self.split_path(path)
34
+ ext = File.extname(path)
35
+ dirname = File.dirname(path)
36
+ dirname = '' if dirname == '.'
37
+ [dirname, File.basename(path, ext), ext]
38
+ end
39
+
40
+ def self.validate_config
41
+ unless @config
42
+ raise 'Fir cannot start unless you call Fir.config in config.rb.'
43
+ end
44
+ missing_options = [:site_name, :perform_caching, :relative_url_root].inject([]) do |missing, opt|
45
+ missing << opt if @config.send(opt).nil?
46
+ missing
47
+ end
48
+ unless missing_options.empty?
49
+ raise "Fir.config (in config.rb) is missing the following options: #{missing_options.join(', ')}"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'fir'
3
+
4
+ ::Fir::Tasks.define
@@ -0,0 +1,8 @@
1
+ # This is the main config file for your site. Add any "require" statements or any other global directives here.
2
+
3
+ require 'maruku' # Comment out if you don't want to use Markdown
4
+
5
+ Fir.config do |config|
6
+ config.site_name = 'Change me in config.rb'
7
+ config.perform_caching = false
8
+ end
@@ -0,0 +1,12 @@
1
+ # This file tells Rack how to boot Fir. It's the point of entry for the whole framework.
2
+ # You shouldn't have to modify this file. If you came here looking for the app's config
3
+ # file, you're in the wrong place. Look at config.rb instead.
4
+
5
+ FIR_ROOT = File.expand_path(File.dirname(__FILE__))
6
+
7
+ require 'fir.rb'
8
+ require 'config.rb'
9
+
10
+ # This is where it all begins.
11
+ # See boot.rb in the Fir gem if you're curious about what happens next.
12
+ instance_eval(&Fir.boot_proc)
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml">
4
+
5
+ <head>
6
+ <title>
7
+ <% if @title and !@title.empty? %>
8
+ <%= h @title %> -
9
+ <% end %>
10
+ My Site
11
+ </title>
12
+ <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
13
+ <% if @description and !@description.empty? %>
14
+ <meta name="description" content="<%= h @page_description %>"/>
15
+ <% end %>
16
+ <link href="/stylesheets/style.css" rel="stylesheet" type="text/css"/>
17
+ </head>
18
+
19
+ <body>
20
+
21
+ <div id="container">
22
+ <ul id="top_nav">
23
+ <% menu('main') do |item, current_page| %>
24
+ <!-- current_page is true if the menu item links to the current page, false otherwise.
25
+ It's usually not necessary, since you'll automatically get either an <a> or <span class="selected">.-->
26
+ <li><%= item %></li>
27
+ <% end %>
28
+ </ul>
29
+
30
+ <%= yield %>
31
+ </div>
32
+
33
+ </body>
34
+ </html>
@@ -0,0 +1,5 @@
1
+ # You must restart your server after editing this file.
2
+
3
+ main:
4
+ - Home:
5
+ - Contact: contact
@@ -0,0 +1,6 @@
1
+ # You must restart your server after editing this file.
2
+
3
+ index:
4
+ title: Home
5
+ contact:
6
+ title: Contact
@@ -0,0 +1 @@
1
+ <h1>Sorry, there is nothing at this address. (404 Not Found)</h1>
@@ -0,0 +1,2 @@
1
+ Contact
2
+ =======
@@ -0,0 +1,2 @@
1
+ Home
2
+ ====
@@ -0,0 +1,8 @@
1
+ body { font-family: helvetica, arial, verdana, sans-serif; font-size: 83%; text-align: center; }
2
+ /* General typography */
3
+ h1 { font-size: 2em; }
4
+ h2 { font-size: 1.7em; }
5
+ h3 { font-size: 1.5em; }
6
+ /* Layout elements */
7
+ div#container { width: 960px; margin-left: auto; margin-right: auto; text-align: left; }
8
+ /* Specific pages */
@@ -0,0 +1 @@
1
+ Run "touch tmp/restart.txt" to restart Passenger. (This doesn't work if you stared Fir using rackup.)
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe 'Fir' do
4
+ it 'returns static files in the public directory'
5
+
6
+ it 'returns static files in subdirectories of the public directory'
7
+
8
+ it 'does not allow access to files above the pages directory'
9
+
10
+ it 'maps pretty URLs to files in the pages directory'
11
+
12
+ it 'maps pretty URLs to files in subdirectories of the pages directory'
13
+
14
+ it 'returns 404 for bad URLs'
15
+
16
+ context 'with caching on' do
17
+ it 'uses the cached page for pages in the root directory'
18
+
19
+ it 'uses the cached page for pages in subdirectories'
20
+
21
+ it 'writes to the cache when a page is accessed'
22
+ end
23
+
24
+ describe 'page_config.yml' do
25
+ it 'can override URL mappings'
26
+
27
+ it 'can specify page titles (if implemented in the layout)'
28
+
29
+ it 'can specify meta descriptions (if implemented in the layout)'
30
+ end
31
+
32
+ describe 'admin interface' do
33
+ it 'returns 404 for bad URLs'
34
+
35
+ describe 'GET pages' do
36
+ it 'requires HTTP auth'
37
+
38
+ it 'does not accept bad user/pass combos'
39
+
40
+ it 'returns an XML feed with each directory and page'
41
+ end
42
+
43
+ describe 'GET page/pagename' do
44
+ it 'requires HTTP auth'
45
+
46
+ it 'does not accept bad user/pass combos'
47
+
48
+ it 'returns the source code of the page'
49
+
50
+ it 'sets Content-Type to text/plain'
51
+
52
+ it 'sets Content-Disposition to attachment'
53
+
54
+ it 'sets Content-Disposition filename to the source filename'
55
+ end
56
+
57
+ describe 'POST page/pagename' do
58
+ it 'requires HTTP auth'
59
+
60
+ it 'does not accept bad user/pass combos'
61
+
62
+ it 'creates the page if none exists'
63
+
64
+ it 'overwrites the page if it exists'
65
+
66
+ it 'clears the cache if caching is on'
67
+
68
+ it "clears the cache even if caching is off (in case it's off temporarily)"
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'fir'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fir
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - jarrett
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-01 00:00:00 -06:00
18
+ default_executable: fir
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 2
30
+ - 9
31
+ version: 1.2.9
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rack
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 0
44
+ - 1
45
+ version: 1.0.1
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: builder
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 2
57
+ - 1
58
+ - 2
59
+ version: 2.1.2
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: maruku
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 0
71
+ - 6
72
+ - 0
73
+ version: 0.6.0
74
+ type: :runtime
75
+ version_requirements: *id004
76
+ description: Rack-based, Passenger-compatible, with pretty URLs and simple HTTP API for managing content
77
+ email: jarrett@uchicago.edu
78
+ executables:
79
+ - fir
80
+ extensions: []
81
+
82
+ extra_rdoc_files:
83
+ - LICENSE
84
+ - README.markdown
85
+ files:
86
+ - bin/fir
87
+ - example/Rakefile
88
+ - example/boot_example.rb
89
+ - example/config.rb
90
+ - example/config.ru
91
+ - example/layouts/application.html.erb
92
+ - example/menus.yml
93
+ - example/pages.yml
94
+ - example/pages/404.html.erb
95
+ - example/pages/contact-us.markdown
96
+ - example/pages/index.markdown
97
+ - example/pages/products/dynamite-plus.html.erb
98
+ - example/pages/products/dynamite.html.erb
99
+ - example/pages/products/index.markdown
100
+ - example/pages/products/mouse-trap.html.erb
101
+ - example/public/manage-content.html
102
+ - example/public/static.txt
103
+ - example/public/stylesheets/style.css
104
+ - example/tmp/restart.txt
105
+ - example/users.yml
106
+ - lib/fir.rb
107
+ - lib/fir/admin.rb
108
+ - lib/fir/boot.rb
109
+ - lib/fir/helpers.rb
110
+ - lib/fir/pages.rb
111
+ - lib/fir/static.rb
112
+ - lib/fir/tasks.rb
113
+ - lib/fir/util.rb
114
+ - skeleton/Rakefile
115
+ - skeleton/config.rb
116
+ - skeleton/config.ru
117
+ - skeleton/layouts/application.html.erb
118
+ - skeleton/menus.yml
119
+ - skeleton/pages.yml
120
+ - skeleton/pages/404.html.erb
121
+ - skeleton/pages/contact.markdown
122
+ - skeleton/pages/index.markdown
123
+ - skeleton/public/stylesheets/style.css
124
+ - skeleton/tmp/restart.txt
125
+ - LICENSE
126
+ - README.markdown
127
+ has_rdoc: true
128
+ homepage: http://github.com/jarrett/fir
129
+ licenses: []
130
+
131
+ post_install_message:
132
+ rdoc_options:
133
+ - --charset=UTF-8
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ segments:
141
+ - 0
142
+ version: "0"
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ segments:
148
+ - 0
149
+ version: "0"
150
+ requirements: []
151
+
152
+ rubyforge_project:
153
+ rubygems_version: 1.3.6
154
+ signing_key:
155
+ specification_version: 3
156
+ summary: A minimalist Ruby framework for small, static sites
157
+ test_files:
158
+ - spec/fir_spec.rb
159
+ - spec/spec_helper.rb