bonsai 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/.kick +26 -0
  4. data/LICENSE +20 -0
  5. data/README.md +65 -0
  6. data/Rakefile +64 -0
  7. data/VERSION +1 -0
  8. data/benchmark/associations.rb +51 -0
  9. data/bin/bonsai +69 -0
  10. data/bonsai.gemspec +142 -0
  11. data/lib/bonsai.rb +60 -0
  12. data/lib/bonsai/console.rb +8 -0
  13. data/lib/bonsai/exporter.rb +103 -0
  14. data/lib/bonsai/generate.rb +24 -0
  15. data/lib/bonsai/navigation.rb +7 -0
  16. data/lib/bonsai/page.rb +192 -0
  17. data/lib/bonsai/template.rb +31 -0
  18. data/lib/bonsai/templates/content/index/default.yml +3 -0
  19. data/lib/bonsai/templates/public/.htaccess +28 -0
  20. data/lib/bonsai/templates/public/docs/css/base.less +1 -0
  21. data/lib/bonsai/templates/templates/default.mustache +14 -0
  22. data/lib/bonsai/webserver.rb +19 -0
  23. data/spec/bonsai/console_spec.rb +12 -0
  24. data/spec/bonsai/exporter_spec.rb +142 -0
  25. data/spec/bonsai/generate_spec.rb +40 -0
  26. data/spec/bonsai/navigation_spec.rb +28 -0
  27. data/spec/bonsai/page_spec.rb +225 -0
  28. data/spec/bonsai/template_spec.rb +19 -0
  29. data/spec/bonsai_spec.rb +21 -0
  30. data/spec/spec.opts +1 -0
  31. data/spec/spec_helper.rb +14 -0
  32. data/spec/support/broken/content/broken_page/demo-template.yml +3 -0
  33. data/spec/support/content/1.about-us/1.contact/1.child/a_file_asset.txt +0 -0
  34. data/spec/support/content/1.about-us/1.contact/1.child/demo-template.yml +1 -0
  35. data/spec/support/content/1.about-us/1.contact/demo-template.yml +7 -0
  36. data/spec/support/content/1.about-us/1.contact/images/image001.jpg +0 -0
  37. data/spec/support/content/1.about-us/1.contact/magic/image001.jpg +0 -0
  38. data/spec/support/content/1.about-us/1.contact/magic/image002.jpg +0 -0
  39. data/spec/support/content/1.about-us/demo-template.yml +1 -0
  40. data/spec/support/content/1.about-us/history/a_file_asset.txt +0 -0
  41. data/spec/support/content/1.about-us/history/child/a_file_asset.txt +0 -0
  42. data/spec/support/content/1.about-us/history/child/demo-template.yml +1 -0
  43. data/spec/support/content/1.about-us/history/demo-template.yml +1 -0
  44. data/spec/support/content/1.about-us/history/image001.jpg +0 -0
  45. data/spec/support/content/1.about-us/history/images/image001.jpg +0 -0
  46. data/spec/support/content/1.about-us/history/magic/image001.jpg +0 -0
  47. data/spec/support/content/1.about-us/history/magic/image002.jpg +0 -0
  48. data/spec/support/content/10.many-pages/demo-template.yml +1 -0
  49. data/spec/support/content/2.products/1.product-a/demo-template.yml +1 -0
  50. data/spec/support/content/2.products/2.product-b/demo-template.yml +1 -0
  51. data/spec/support/content/2.products/demo-template.yml +1 -0
  52. data/spec/support/content/index/demo-template.yml +1 -0
  53. data/spec/support/content/legals/terms-and-conditions/demo-template.yml +1 -0
  54. data/spec/support/public/.htaccess +31 -0
  55. data/spec/support/public/js/script.js +19 -0
  56. data/spec/support/public/stylesheets/base.less +2 -0
  57. data/spec/support/public/stylesheets/broken.less +2 -0
  58. data/spec/support/templates/demo-template.mustache +19 -0
  59. data/spec/support/templates/partials/inserted.mustache +1 -0
  60. data/vendor/yui-compressor/yuicompressor-2.4.2.jar +0 -0
  61. 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,8 @@
1
+ require 'irb'
2
+ module Bonsai
3
+ class Console
4
+ def initialize
5
+ IRB.start(__FILE__)
6
+ end
7
+ end
8
+ 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
@@ -0,0 +1,7 @@
1
+ module Bonsai
2
+ class Navigation
3
+ def self.tree
4
+ Page.all(Page.path, "*").select{|p| File.dirname(p.disk_path).match(/\d.*$/) }
5
+ end
6
+ end
7
+ end
@@ -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,3 @@
1
+ headline: Welcome to Bonsai
2
+ body: |
3
+ A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away.
@@ -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