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.
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