bonsai 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +23 -0
- data/.kick +26 -0
- data/LICENSE +20 -0
- data/README.md +65 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/benchmark/associations.rb +51 -0
- data/bin/bonsai +69 -0
- data/bonsai.gemspec +142 -0
- data/lib/bonsai.rb +60 -0
- data/lib/bonsai/console.rb +8 -0
- data/lib/bonsai/exporter.rb +103 -0
- data/lib/bonsai/generate.rb +24 -0
- data/lib/bonsai/navigation.rb +7 -0
- data/lib/bonsai/page.rb +192 -0
- data/lib/bonsai/template.rb +31 -0
- data/lib/bonsai/templates/content/index/default.yml +3 -0
- data/lib/bonsai/templates/public/.htaccess +28 -0
- data/lib/bonsai/templates/public/docs/css/base.less +1 -0
- data/lib/bonsai/templates/templates/default.mustache +14 -0
- data/lib/bonsai/webserver.rb +19 -0
- data/spec/bonsai/console_spec.rb +12 -0
- data/spec/bonsai/exporter_spec.rb +142 -0
- data/spec/bonsai/generate_spec.rb +40 -0
- data/spec/bonsai/navigation_spec.rb +28 -0
- data/spec/bonsai/page_spec.rb +225 -0
- data/spec/bonsai/template_spec.rb +19 -0
- data/spec/bonsai_spec.rb +21 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/broken/content/broken_page/demo-template.yml +3 -0
- data/spec/support/content/1.about-us/1.contact/1.child/a_file_asset.txt +0 -0
- data/spec/support/content/1.about-us/1.contact/1.child/demo-template.yml +1 -0
- data/spec/support/content/1.about-us/1.contact/demo-template.yml +7 -0
- data/spec/support/content/1.about-us/1.contact/images/image001.jpg +0 -0
- data/spec/support/content/1.about-us/1.contact/magic/image001.jpg +0 -0
- data/spec/support/content/1.about-us/1.contact/magic/image002.jpg +0 -0
- data/spec/support/content/1.about-us/demo-template.yml +1 -0
- data/spec/support/content/1.about-us/history/a_file_asset.txt +0 -0
- data/spec/support/content/1.about-us/history/child/a_file_asset.txt +0 -0
- data/spec/support/content/1.about-us/history/child/demo-template.yml +1 -0
- data/spec/support/content/1.about-us/history/demo-template.yml +1 -0
- data/spec/support/content/1.about-us/history/image001.jpg +0 -0
- data/spec/support/content/1.about-us/history/images/image001.jpg +0 -0
- data/spec/support/content/1.about-us/history/magic/image001.jpg +0 -0
- data/spec/support/content/1.about-us/history/magic/image002.jpg +0 -0
- data/spec/support/content/10.many-pages/demo-template.yml +1 -0
- data/spec/support/content/2.products/1.product-a/demo-template.yml +1 -0
- data/spec/support/content/2.products/2.product-b/demo-template.yml +1 -0
- data/spec/support/content/2.products/demo-template.yml +1 -0
- data/spec/support/content/index/demo-template.yml +1 -0
- data/spec/support/content/legals/terms-and-conditions/demo-template.yml +1 -0
- data/spec/support/public/.htaccess +31 -0
- data/spec/support/public/js/script.js +19 -0
- data/spec/support/public/stylesheets/base.less +2 -0
- data/spec/support/public/stylesheets/broken.less +2 -0
- data/spec/support/templates/demo-template.mustache +19 -0
- data/spec/support/templates/partials/inserted.mustache +1 -0
- data/vendor/yui-compressor/yuicompressor-2.4.2.jar +0 -0
- metadata +201 -0
data/lib/bonsai.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
$LOAD_PATH << "#{File.dirname(__FILE__)}/bonsai"
|
6
|
+
|
7
|
+
module Bonsai
|
8
|
+
@@root_dir = nil
|
9
|
+
@@config = { :enable_logging => true }
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def root_dir
|
13
|
+
@@root_dir || Dir.pwd
|
14
|
+
end
|
15
|
+
|
16
|
+
def root_dir=(path)
|
17
|
+
unless is_a_bonsai?(path)
|
18
|
+
log "no bonsai site found - are you in the right directory?"
|
19
|
+
exit 0
|
20
|
+
end
|
21
|
+
|
22
|
+
@@root_dir = path
|
23
|
+
|
24
|
+
Exporter.path = "#{path}/output"
|
25
|
+
Page.path = "#{path}/content"
|
26
|
+
Template.path = "#{path}/templates"
|
27
|
+
end
|
28
|
+
|
29
|
+
def log(message)
|
30
|
+
puts message if @@config[:enable_logging]
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
@@config
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure(&block)
|
38
|
+
yield @@config
|
39
|
+
end
|
40
|
+
|
41
|
+
def version
|
42
|
+
File.read("#{File.dirname(__FILE__)}/../VERSION")
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def is_a_bonsai?(path)
|
47
|
+
File.directory?("#{path}/content") && File.directory?("#{path}/public") && File.directory?("#{path}/templates")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
autoload :Page, "page"
|
52
|
+
autoload :Console, "console"
|
53
|
+
autoload :Exporter, "exporter"
|
54
|
+
autoload :Template, "template"
|
55
|
+
autoload :Generate, "generate"
|
56
|
+
autoload :Navigation, "navigation"
|
57
|
+
autoload :PagePresenter, "page_presenter"
|
58
|
+
autoload :StaticPassThrough, "webserver"
|
59
|
+
autoload :DevelopmentServer, "webserver"
|
60
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'less'
|
3
|
+
|
4
|
+
module Bonsai
|
5
|
+
class Exporter
|
6
|
+
@@path = "output"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def path; @@path; end
|
10
|
+
def path=(path); @@path = path; end
|
11
|
+
|
12
|
+
def process!
|
13
|
+
setup
|
14
|
+
copy_public
|
15
|
+
copy_assets
|
16
|
+
cleanup
|
17
|
+
end
|
18
|
+
|
19
|
+
def publish!
|
20
|
+
teardown
|
21
|
+
setup
|
22
|
+
copy_assets
|
23
|
+
copy_public
|
24
|
+
compress_assets
|
25
|
+
write_index
|
26
|
+
write_pages
|
27
|
+
cleanup
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
def teardown
|
32
|
+
FileUtils.rm_rf path
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup
|
36
|
+
FileUtils.mkdir_p path
|
37
|
+
end
|
38
|
+
|
39
|
+
def cleanup
|
40
|
+
remove_less_from_public
|
41
|
+
end
|
42
|
+
|
43
|
+
def write_index
|
44
|
+
Bonsai.log "Writing index"
|
45
|
+
File.open("#{path}/index.html", "w") {|file| file.write(Page.find("index").render)}
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_pages
|
49
|
+
Bonsai.log "Writing pages"
|
50
|
+
Page.all.each do |page|
|
51
|
+
Bonsai.log "\t Writing page - #{page.permalink}"
|
52
|
+
FileUtils.mkdir_p("#{path}#{page.permalink}")
|
53
|
+
File.open("#{path}#{page.write_path}", "w"){|file| file.write(page.render) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def copy_assets
|
58
|
+
Bonsai.log "Copying page assets"
|
59
|
+
Page.all.each do |page|
|
60
|
+
page.assets.each do |asset|
|
61
|
+
# Create the path to the asset by the export path of the page + File.dirname(asset permalink)
|
62
|
+
FileUtils.mkdir_p "#{path}#{File.dirname(asset[:path])}"
|
63
|
+
|
64
|
+
# Copy the the asset from its disk path to File.dirname(asset permalink)
|
65
|
+
FileUtils.cp asset[:disk_path], "#{path}#{asset[:path]}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def copy_public
|
71
|
+
generate_css_from_less
|
72
|
+
|
73
|
+
Bonsai.log "Copying public files"
|
74
|
+
# Using system call because fileutils is inadequate
|
75
|
+
system("cp -fR '#{Bonsai.root_dir}/public/' '#{path}'")
|
76
|
+
end
|
77
|
+
|
78
|
+
def compress_assets
|
79
|
+
yui_compressor = File.expand_path("#{File.dirname(__FILE__)}/../../vendor/yui-compressor/yuicompressor-2.4.2.jar")
|
80
|
+
|
81
|
+
Bonsai.log "Compressing javascript and stylesheets"
|
82
|
+
Dir["#{path}/**/*.{js,css}"].each do |asset|
|
83
|
+
system "java -jar #{yui_compressor} #{File.expand_path(asset)} -o #{File.expand_path(asset)}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_css_from_less
|
88
|
+
Dir["#{Bonsai.root_dir}/public/**/*.less"].each do |lessfile|
|
89
|
+
css = File.open(lessfile) {|f| Less::Engine.new(f) }.to_css
|
90
|
+
path = "#{File.dirname(lessfile)}/#{File.basename(lessfile, ".less")}.css"
|
91
|
+
|
92
|
+
File.open(path, "w") {|file| file.write(css) }
|
93
|
+
end
|
94
|
+
rescue Less::SyntaxError => exception
|
95
|
+
Bonsai.log "LessCSS Syntax error\n\n#{exception.message}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_less_from_public
|
99
|
+
Dir["#{path}/**/*.less"].each{|f| FileUtils.rm(f) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Bonsai
|
2
|
+
class Generate
|
3
|
+
def initialize(path)
|
4
|
+
@path = path
|
5
|
+
|
6
|
+
Bonsai.log "Planting your bonsai '#{path}'"
|
7
|
+
copy_templates
|
8
|
+
create_directories
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def create_directories
|
13
|
+
%w(content content/index public/docs/css public/docs/js).each do |dir|
|
14
|
+
Bonsai.log "\tcreate\t#{dir}"
|
15
|
+
FileUtils.mkdir_p(File.join(@path, dir))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_templates
|
20
|
+
# Using system call because fileutils is inadequate
|
21
|
+
system("cp -fR '#{File.expand_path("#{File.dirname(__FILE__)}/templates")}' '#{@path}'")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/bonsai/page.rb
ADDED
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'rdiscount'
|
3
|
+
require 'tilt'
|
4
|
+
|
5
|
+
module Bonsai
|
6
|
+
class Page
|
7
|
+
class NotFound < StandardError; end;
|
8
|
+
class PropertyNotFound < StandardError; end
|
9
|
+
@@pages = {}
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def path; @@path; end
|
13
|
+
def path=(path); @@path = path; end
|
14
|
+
|
15
|
+
def all(dir_path = path, pattern = "*/**")
|
16
|
+
Dir["#{dir_path}/#{pattern}/*.yml"].map {|p| Page.new p }
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(permalink)
|
20
|
+
@@pages[permalink] ||= find!(permalink)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def find!(permalink)
|
25
|
+
search_path = permalink.gsub("/", "/*")
|
26
|
+
disk_path = Dir["#{path}/*#{search_path}/*.yml"]
|
27
|
+
if disk_path.any?
|
28
|
+
return new disk_path.first
|
29
|
+
else
|
30
|
+
raise NotFound, "page '#{permalink}' not found at '#{path}'"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :disk_path
|
36
|
+
|
37
|
+
def initialize(path)
|
38
|
+
@disk_path = path
|
39
|
+
end
|
40
|
+
|
41
|
+
def slug
|
42
|
+
permalink.split('/').pop
|
43
|
+
end
|
44
|
+
|
45
|
+
def name
|
46
|
+
slug.gsub(/\W/, " ").gsub(/\d\./, '').gsub(/^\w/){$&.upcase}
|
47
|
+
end
|
48
|
+
|
49
|
+
def permalink
|
50
|
+
web_path(directory)
|
51
|
+
end
|
52
|
+
|
53
|
+
def write_path
|
54
|
+
"#{permalink}/index.html"
|
55
|
+
end
|
56
|
+
|
57
|
+
def template
|
58
|
+
Template.find(template_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
# This method is used for the exporter to copy assets
|
62
|
+
def assets
|
63
|
+
Dir["#{directory}/**/*"].select{|p| !File.directory?(p) && !File.basename(p).include?("yml") }.map do |a|
|
64
|
+
{
|
65
|
+
:name => File.basename(a),
|
66
|
+
:path => web_path(a),
|
67
|
+
:disk_path => a
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def floating?
|
73
|
+
!!(File.dirname(disk_path) =~ /\/[a-zA-z][\w-]+$/)
|
74
|
+
end
|
75
|
+
|
76
|
+
def parent
|
77
|
+
id = permalink[/^\/(.+)\/[^\/]*$/, 1]
|
78
|
+
return nil if id.nil?
|
79
|
+
|
80
|
+
parent = Page.find(id)
|
81
|
+
return nil if parent == self
|
82
|
+
|
83
|
+
parent
|
84
|
+
rescue NotFound
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def siblings
|
89
|
+
self.class.all(File.dirname(disk_path[/(.+)\/[^\/]*$/, 1]), "*").delete_if{|p| p == self}
|
90
|
+
end
|
91
|
+
|
92
|
+
def children
|
93
|
+
self.class.all(File.dirname(disk_path), "*").delete_if{|p| p.floating? }
|
94
|
+
end
|
95
|
+
|
96
|
+
def ancestors
|
97
|
+
ancestors = []
|
98
|
+
# Remove leading slash
|
99
|
+
page_ref = permalink.gsub(/^\//, '')
|
100
|
+
|
101
|
+
# Find pages up the permalink tree if possible
|
102
|
+
while(page_ref) do
|
103
|
+
page_ref = page_ref[/(.+)\/[^\/]*$/, 1]
|
104
|
+
ancestors << self.class.find(page_ref) rescue nil
|
105
|
+
end
|
106
|
+
|
107
|
+
ancestors.compact.reverse
|
108
|
+
end
|
109
|
+
|
110
|
+
def ==(other)
|
111
|
+
self.permalink == other.permalink
|
112
|
+
end
|
113
|
+
|
114
|
+
def render
|
115
|
+
Tilt.new(template.path, :path => template.class.path).render(self, to_hash)
|
116
|
+
rescue => stack
|
117
|
+
raise "Issue rendering #{permalink}\n\n#{stack}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def content
|
121
|
+
YAML::load(File.read(@disk_path)) || {}
|
122
|
+
rescue ArgumentError
|
123
|
+
Bonsai.log "Page '#{permalink}' has badly formatted content"
|
124
|
+
end
|
125
|
+
|
126
|
+
# This hash is available to all templates, it will map common properties,
|
127
|
+
# content file results, as well as any "magic" hashes for file
|
128
|
+
# system contents
|
129
|
+
def to_hash
|
130
|
+
{
|
131
|
+
:slug => slug,
|
132
|
+
:permalink => permalink,
|
133
|
+
:name => name,
|
134
|
+
:children => children,
|
135
|
+
:siblings => siblings,
|
136
|
+
:parent => parent,
|
137
|
+
:ancestors => ancestors,
|
138
|
+
:navigation => Bonsai::Navigation.tree
|
139
|
+
}.merge(formatted_content).merge(disk_assets)
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
def formatted_content
|
144
|
+
formatted_content = content
|
145
|
+
|
146
|
+
formatted_content.each do |k,v|
|
147
|
+
if v.is_a?(String) and v =~ /\n/
|
148
|
+
formatted_content[k] = RDiscount.new(v, :smart).to_html
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
formatted_content
|
153
|
+
end
|
154
|
+
|
155
|
+
# Creates methods for each sub-folder within the page's folder
|
156
|
+
# that isn't a sub-page (a page object)
|
157
|
+
def disk_assets
|
158
|
+
assets = {}
|
159
|
+
Dir["#{File.dirname(disk_path)}/**"].select{|p| File.directory?(p)}.reject {|p|
|
160
|
+
Dir.entries(p).any?{|e| e.include? "yml"}
|
161
|
+
}.each{|asset_path| assets.merge!(map_to_disk(asset_path)) }
|
162
|
+
|
163
|
+
assets
|
164
|
+
end
|
165
|
+
|
166
|
+
def map_to_disk(path)
|
167
|
+
name = File.basename(path)
|
168
|
+
|
169
|
+
{
|
170
|
+
name.to_sym => Dir["#{path}/*"].map do |file|
|
171
|
+
{
|
172
|
+
:name => File.basename(file),
|
173
|
+
:path => web_path(file),
|
174
|
+
:disk_path => file
|
175
|
+
}
|
176
|
+
end
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
def directory
|
181
|
+
@disk_path.split("/")[0..-2].join("/")
|
182
|
+
end
|
183
|
+
|
184
|
+
def template_name
|
185
|
+
File.basename(@disk_path, '.*')
|
186
|
+
end
|
187
|
+
|
188
|
+
def web_path(path)
|
189
|
+
path.gsub(self.class.path, '').gsub(/\/\d+\./, '/')
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Bonsai
|
2
|
+
class Template
|
3
|
+
@@path = "templates"
|
4
|
+
|
5
|
+
class NotFound < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def path; @@path; end
|
9
|
+
|
10
|
+
def path=(path)
|
11
|
+
@@path = path
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(name)
|
15
|
+
disk_path = Dir["#{path}/#{name}.*"]
|
16
|
+
|
17
|
+
if disk_path.any?
|
18
|
+
new disk_path.first
|
19
|
+
else
|
20
|
+
raise NotFound, "template '#{name}' not found at #{path}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :path
|
26
|
+
|
27
|
+
def initialize(path)
|
28
|
+
@path = path
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
DirectoryIndex index.html
|
2
|
+
FileETag All
|
3
|
+
|
4
|
+
# Compress all static assets
|
5
|
+
<IfModule mod_deflate.c>
|
6
|
+
# compress content with type html, text, and css
|
7
|
+
AddOutputFilterByType DEFLATE text/css text/html text/javascript application/javascript application/x-javascript text/js text/plain text/xml
|
8
|
+
|
9
|
+
<IfModule mod_headers.c>
|
10
|
+
# properly handle requests coming from behind proxies
|
11
|
+
Header append Vary User-Agent
|
12
|
+
</IfModule>
|
13
|
+
</IfModule>
|
14
|
+
|
15
|
+
# Cache, aggressively
|
16
|
+
<IfModule mod_expires.c>
|
17
|
+
ExpiresActive On
|
18
|
+
ExpiresDefault "access plus 10 days"
|
19
|
+
|
20
|
+
<FilesMatch "\.(eot|ttf|otf)$">
|
21
|
+
ExpiresDefault "access plus 10 years"
|
22
|
+
</filesMatch>
|
23
|
+
</IfModule>
|
24
|
+
|
25
|
+
# Mime-types
|
26
|
+
AddType application/vnd.ms-fontobject .eot
|
27
|
+
AddType font/ttf .ttf
|
28
|
+
AddType font/otf .otf
|