bonsai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|