ichiban 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +4 -0
  3. data/README +56 -0
  4. data/bin/ichiban.rb +6 -0
  5. data/ichiban.gemspec +25 -0
  6. data/klass.rb +7 -0
  7. data/lib/ichiban/command.rb +11 -0
  8. data/lib/ichiban/compilation.rb +93 -0
  9. data/lib/ichiban/config.rb +31 -0
  10. data/lib/ichiban/erb_page.rb +16 -0
  11. data/lib/ichiban/files.rb +105 -0
  12. data/lib/ichiban/helpers.rb +151 -0
  13. data/lib/ichiban/layouts.rb +10 -0
  14. data/lib/ichiban/loading.rb +19 -0
  15. data/lib/ichiban/logger.rb +19 -0
  16. data/lib/ichiban/mapping.rb +38 -0
  17. data/lib/ichiban/path.rb +55 -0
  18. data/lib/ichiban/script_runner.rb +33 -0
  19. data/lib/ichiban/tasks.rb +23 -0
  20. data/lib/ichiban/version.rb +3 -0
  21. data/lib/ichiban/watcher.rb +7 -0
  22. data/lib/ichiban.rb +25 -0
  23. data/sample/Rakefile +2 -0
  24. data/sample/compiled/about.html +13 -0
  25. data/sample/compiled/bad.html +0 -0
  26. data/sample/compiled/images/check.png +0 -0
  27. data/sample/compiled/index.html +13 -0
  28. data/sample/compiled/javascripts/interaction.js +1 -0
  29. data/sample/compiled/staff/_employee.html +13 -0
  30. data/sample/compiled/staff/andre-marques.html +13 -0
  31. data/sample/compiled/staff/index.html +17 -0
  32. data/sample/compiled/staff/jarrett-colby.html +13 -0
  33. data/sample/compiled/stylesheets/reset.css +1 -0
  34. data/sample/compiled/stylesheets/screen.css +1 -0
  35. data/sample/config.rb +3 -0
  36. data/sample/content/about.html +1 -0
  37. data/sample/content/bad.html +3 -0
  38. data/sample/content/index.html +1 -0
  39. data/sample/content/staff/_employee.html +1 -0
  40. data/sample/content/staff/index.html +6 -0
  41. data/sample/data/employees.csv +2 -0
  42. data/sample/errors/404.html +1 -0
  43. data/sample/helpers/staff_helper.rb +5 -0
  44. data/sample/images/check.png +0 -0
  45. data/sample/javascripts/interaction.js +1 -0
  46. data/sample/layouts/default.html +13 -0
  47. data/sample/models/employee.rb +16 -0
  48. data/sample/scripts/bad.rb +1 -0
  49. data/sample/scripts/staff.rb +8 -0
  50. data/sample/stylesheets/reset.css +1 -0
  51. data/sample/stylesheets/screen.scss +5 -0
  52. data/spec/integration_spec.rb +89 -0
  53. data/spec/path_spec.rb +15 -0
  54. data/spec/spec_helper.rb +19 -0
  55. metadata +136 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ .sass-cache
5
+ .sass-cache/**/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in ichiban.gemspec
4
+ gemspec
data/README ADDED
@@ -0,0 +1,56 @@
1
+ To use, create a folder for your project. Populate it with the appropriate folders. Create a Rakefile and add this:
2
+
3
+ require 'ichiban'
4
+ Ichiban::Tasks.new.define
5
+
6
+ Overall design
7
+ ==============
8
+
9
+ The Rakefile should have as little in it as possible.
10
+
11
+ Each individual bit of functionality should be separate and self-sufficient, although they can all depend on a configured project.
12
+
13
+ For each of the following:
14
+
15
+ - Content file
16
+ - Stylesheet
17
+ - JS
18
+ - Image
19
+
20
+ ...do each of these:
21
+
22
+ - Compile/copy one
23
+ - List all
24
+ - List stale
25
+ - Compile/copy all
26
+ - Compile/copy stale
27
+
28
+ Listing is done in files.rb. Compiling is done in compilation.rb.
29
+
30
+ Further:
31
+
32
+ - List all data files
33
+ - Run scripts
34
+
35
+ The compilation logger needs to be accessible from everywhere so that generators etc. can get to it.
36
+
37
+ The watcher should delete files in the compiled directory when they're no longer needed. This can't be automatically determined for files based on scripts.
38
+
39
+ The watcher should use the fork process described here:
40
+
41
+ http://rkh.im/code-reloading
42
+
43
+ Initially, the watcher forks. Both the parent and child process watch for changes, but they respond differently. The parent's only job is to kill the
44
+ child and refork upon changes to models, scripts, and helpers. The child's job is to compile.
45
+
46
+ Scripts should have an optional dependencies block:
47
+
48
+ dependencies('data/employees.csv', 'data/stores.csv') do
49
+ # Generate calls go here
50
+ end
51
+
52
+ This will cause the block to run only when employees.csv or stores.csv has changed. Since there's no way to infer the output file paths,
53
+ we need to simply keep track of when the script was last run. Then, if any of the dependencies have changed after that, we re-run the script.
54
+ To track this, we'll store a JSON hash in scripts/.last_runs
55
+
56
+ {'employees.rb': 1316041807}
data/bin/ichiban.rb ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'ichiban'
5
+
6
+ Ichiban::Command.new(ARGV).run
data/ichiban.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ichiban/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ichiban"
7
+ s.version = Ichiban::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Jarrett Colby"]
10
+ s.email = ["jarrettcolby@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Static website compiler}
13
+ s.description = %q{The most elegant way to compile static websites}
14
+
15
+ s.rubyforge_project = "ichiban"
16
+
17
+ s.add_dependency 'activesupport'
18
+ s.add_dependency 'erubis'
19
+ s.add_dependency 'maruku'
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
data/klass.rb ADDED
@@ -0,0 +1,7 @@
1
+
2
+ class Foo
3
+ def bar
4
+ puts 'bar'
5
+ end
6
+ end
7
+
@@ -0,0 +1,11 @@
1
+ module Ichiban
2
+ class Command
3
+ def initialize(args)
4
+ @args = args
5
+ end
6
+
7
+ def run
8
+ raise 'TODO'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,93 @@
1
+ module Ichiban
2
+ class Compiler
3
+ include Layouts
4
+
5
+ # This should not be called from the watcher. The watcher should have a more fine-grained way of looking at changed files.
6
+ def compile
7
+ load_all_ruby_files
8
+ compile_stale
9
+ run_scripts
10
+ end
11
+
12
+ # Includes, content, images, etc
13
+ def compile_all
14
+ content_files.each { |path| compile_content_file(path) }
15
+ javascripts.each { |path| copy_javascript(path) }
16
+ images.each { |path| copy_image(path) }
17
+ stylesheets.each { |path| copy_or_compile_stylesheet(path) }
18
+ end
19
+
20
+ def compile_content_file(src_path)
21
+ dest_path = content_dest(src_path)
22
+ FileUtils.mkdir_p File.dirname(dest_path.abs)
23
+ File.open(dest_path.replace_ext('html').abs, 'w') do |f|
24
+ begin
25
+ f.write(compile_content_file_to_string(src_path))
26
+ rescue Exception => exc
27
+ Ichiban.logger.exception(exc)
28
+ end
29
+ end
30
+ end
31
+
32
+ def compile_content_file_to_string(src_path)
33
+ erb_context = ErbPage::Context.new(:_current_path => content_dest(src_path).web)
34
+ result = ErbPage.new(File.read(src_path.abs), :filename => src_path.abs).evaluate(erb_context)
35
+ result = case src_path.ext
36
+ when 'markdown'
37
+ Maruku.new(result).to_html
38
+ when 'html'
39
+ result
40
+ else
41
+ raise "compile_file_to_string doesn't know how to handle #{src_path.abs}"
42
+ end
43
+ wrap_in_layouts(erb_context, result)
44
+ end
45
+
46
+ # Includes, content, images, etc
47
+ def compile_stale
48
+ stale_content_files.each { |path| compile_content_file(path) }
49
+ stale_javascripts.each { |path| copy_js(path) }
50
+ stale_images.each { |path| copy_image(path) }
51
+ stale_stylesheets.each { |path| copy_or_compile_stylesheet(path) }
52
+ end
53
+
54
+ def copy_image(path)
55
+ dest = image_dest(path)
56
+ FileUtils.mkdir_p(dest.dirname)
57
+ FileUtils.cp(path.abs, dest.abs)
58
+ end
59
+
60
+ def copy_javascript(path)
61
+ dest = javascript_dest(path)
62
+ FileUtils.mkdir_p(dest.dirname)
63
+ FileUtils.cp(path.abs, dest.abs)
64
+ end
65
+
66
+ def copy_or_compile_stylesheet(path)
67
+ dest = stylesheet_dest(path)
68
+ case path.ext
69
+ when 'css'
70
+ FileUtils.mkdir_p(dest.dirname)
71
+ FileUtils.cp(path.abs, dest.abs)
72
+ when 'scss', 'sass'
73
+ Sass.compile_file(path.abs, dest.abs, :style => :compressed)
74
+ end
75
+ end
76
+
77
+ def fresh
78
+ load_all_ruby_files
79
+ compile_all
80
+ run_scripts
81
+ end
82
+
83
+ def run_scripts
84
+ scripts.each do |path|
85
+ begin
86
+ ScriptRunner.run_script_file(path)
87
+ rescue Exception => exc
88
+ Ichiban.logger.exception(exc)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,31 @@
1
+ module Ichiban
2
+ def self.config
3
+ @config ||= ::Ichiban::Config.new
4
+ yield @config if block_given?
5
+ @config
6
+ end
7
+
8
+ def self.configure_for_project(project_root)
9
+ config.project_root = project_root
10
+ config_file = File.join(project_root, 'config.rb')
11
+ raise "#{config_file} must exist" unless File.exists?(config_file)
12
+ load config_file
13
+ end
14
+
15
+ # It's a bit messy to have this class method that's just an alias to a method on the config object.
16
+ # But so many different bits of code (including client code) need to know the project root, it makes
17
+ # pragmatic sense to have a really compact way to get it.
18
+ def self.project_root
19
+ config.project_root
20
+ end
21
+
22
+ class Config
23
+ attr_accessor :project_root
24
+
25
+ attr_writer :relative_url_root
26
+
27
+ def relative_url_root
28
+ @relative_url_root || raise('Ichiban.config.relative_url_root not set. Set inside block in config.rb like this: cfg.relative_url_root = \'/\'')
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ module Ichiban
2
+ class ErbPage < Erubis::Eruby
3
+ def add_preamble(src)
4
+ src << "@_erb_out = _buf = '';"
5
+ end
6
+
7
+ class Context < Erubis::Context
8
+ include ::Ichiban::Helpers
9
+ include ::Erubis::XmlHelper
10
+
11
+ def layout_stack
12
+ @_layout_stack or ['default']
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,105 @@
1
+ =begin
2
+ - Content file
3
+ - Stylesheet
4
+ - JS
5
+ - Image
6
+
7
+ ...do each of these:
8
+
9
+ - Compile/copy one
10
+ - List all
11
+ - List stale
12
+ - Compile/copy all
13
+ - Compile/copy stale
14
+ =end
15
+
16
+ module Ichiban
17
+ class Compiler
18
+ def content_files
19
+ paths_in('content').reject { |path| path.directory? }
20
+ end
21
+
22
+ def data_files
23
+ paths_in 'data'
24
+ end
25
+
26
+ def helpers
27
+ paths_in 'helpers', 'rb'
28
+ end
29
+
30
+ def images
31
+ paths_in 'images'
32
+ end
33
+
34
+ def models
35
+ paths_in 'models', 'rb'
36
+ end
37
+
38
+ # Returns all stylesheet except for those beginning with an underscore
39
+ def stylesheets
40
+ paths_in('stylesheets').reject { |path| path.filename.start_with?('_') }
41
+ end
42
+
43
+ def javascripts
44
+ paths_in 'javascripts', 'js'
45
+ end
46
+
47
+ def scripts
48
+ paths_in 'scripts', 'rb'
49
+ end
50
+
51
+ # For the stale methods, we rely on the source-destination mapping as provided by mapping.rb.
52
+
53
+ def stale_content_files
54
+ stale(content_files) do |src_path|
55
+ content_dest(src_path)
56
+ end
57
+ end
58
+
59
+ def stale_images
60
+ stale(images) do |src_path|
61
+ image_dest(src_path)
62
+ end
63
+ end
64
+
65
+ def stale_stylesheets
66
+ stale(stylesheets) do |src_path|
67
+ stylesheet_dest(src_path)
68
+ end
69
+ end
70
+
71
+ def stale_javascripts
72
+ stale(javascripts) do |src_path|
73
+ javascript_dest(src_path)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def paths_in(dir, ext = nil)
80
+ pattern = ext ? "**/*.#{ext}" : '**/*'
81
+ Dir.glob(File.join(Ichiban.project_root, dir, pattern)).collect { |abs| Path.new abs }
82
+ end
83
+
84
+ # This method accepts a block. The block will be passed a source file path and must return either a destination path or an array thereof.
85
+ def stale(source_paths)
86
+ source_paths.reject do |path|
87
+ dest = yield(path)
88
+ dest = [dest] unless dest.is_a?(Array)
89
+ up_to_date?(path, *dest)
90
+ end
91
+ end
92
+
93
+ # Accepts one source file and one or more destination files. Pass in Path objects.
94
+ # Note that this method is different from the one in FileUtils: The FileUtils version accepts
95
+ # one destination and multiple sources.
96
+ def up_to_date?(src, *dests)
97
+ src_time = File.mtime(src.abs)
98
+ dests.each do |dest|
99
+ return false if !File.exists?(dest.abs)
100
+ return false unless File.mtime(dest.abs) > src_time
101
+ end
102
+ true
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,151 @@
1
+ module Ichiban
2
+ module Helpers
3
+ def capture
4
+ before_buffering = @_erb_out.dup
5
+ @_erb_out.replace('')
6
+ yield
7
+ captured_output = @_erb_out.dup
8
+ @_erb_out.replace(before_buffering)
9
+ captured_output
10
+ end
11
+
12
+ def concat(str)
13
+ @_erb_out << str
14
+ end
15
+
16
+ def content_for(ivar_name, &block)
17
+ instance_variable_set '@' + ivar_name, capture(&block)
18
+ end
19
+
20
+ def content_tag(*args)
21
+ options = args.extract_options!
22
+ name = args.shift
23
+ content = block_given? ? yield : args.shift
24
+ output = '<' + name
25
+ output << tag_attrs(options) << ">#{content}</#{name}>"
26
+ end
27
+
28
+ # Returns the path relative to site root
29
+ def current_path
30
+ @_current_path
31
+ end
32
+
33
+ def javascript_include_tag(js_file)
34
+ js_file = js_file + '.js' unless js_file.end_with?('.js')
35
+ path = normalize_path(File.join('/javascripts', js_file))
36
+ content_tag 'script', 'type' => 'text/javascript', 'src' => path
37
+ end
38
+
39
+ def layout(*stack)
40
+ @_layout_stack = stack
41
+ end
42
+
43
+ alias_method :layouts, :layout
44
+
45
+ def limit_options(hash, keys = [])
46
+ keys = keys.collect(&:to_s)
47
+ hash.inject({}) do |result, (key, value)|
48
+ result[key] = value if (keys.include?(key.to_s) or (block_given? and yield(key, value)))
49
+ result
50
+ end
51
+ end
52
+
53
+ def link_to(*args)
54
+ options = args.extract_options!
55
+ if args.length == 1
56
+ text = url = args[0]
57
+ else
58
+ text = args[0]
59
+ url = args[1]
60
+ end
61
+ if url.is_a?(String)
62
+ url = normalize_path(url)
63
+ else
64
+ url = url.to_param
65
+ end
66
+ content_tag 'a', text, options.merge('href' => url)
67
+ end
68
+
69
+ # Pass in an array of this form:
70
+ #
71
+ # [
72
+ # ['Link Text', '/path/from/relative/url/root/']
73
+ # ['Link Text', '/path/from/relative/url/root/', {'id' => 'foo'}]
74
+ # ]
75
+ #
76
+ # You can also do this, as a convenience:
77
+ #
78
+ # nav([
79
+ # ['Link Text', 'path/from/section/root/']
80
+ # ], :section => 'section-name')
81
+ #
82
+ # Which will generate this href:
83
+ #
84
+ # /section-name/path/from/section/root/
85
+ #
86
+ # If you don't specify a section, and your URLs don't have leading slashes,
87
+ # the hrefs will use relative URLs.
88
+ def nav(items, options = {})
89
+ ul_options = limit_options(options, %w(id class)) { |key, value| key.to_s.start_with?('data-') }
90
+ content_tag('ul', ul_options) do
91
+ items.inject('') do |lis, (text, path, attrs)|
92
+ if options[:section]
93
+ path = File.join(options[:section], path)
94
+ end
95
+ path = normalize_path(path)
96
+ lis + content_tag('li', (attrs or {})) do
97
+ if path_with_slashes(current_path) == path_with_slashes(path)
98
+ content_tag('span', text, 'class' => 'selected')
99
+ else
100
+ link_to(text, path)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # If the path has a trailing slash, it will be made absolute using relative_url_root.
108
+ # Otherwise, it will remain relative.
109
+ def normalize_path(path)
110
+ if path.start_with?('/')
111
+ File.join(relative_url_root, path)
112
+ else
113
+ path
114
+ end
115
+ end
116
+
117
+ def page_title(title)
118
+ @page_title = title
119
+ end
120
+
121
+ # Adds leading and trailing slashes if none are present
122
+ def path_with_slashes(path)
123
+ path = '/' + path unless path.start_with?('/')
124
+ path << '/' unless path.end_with?('/')
125
+ path
126
+ end
127
+
128
+ def relative_url_root
129
+ Ichiban.config.relative_url_root
130
+ end
131
+
132
+ def stylesheet_link_tag(css_file, media = 'screen')
133
+ css_file = css_file + '.css' unless css_file.end_with?('.css')
134
+ href = normalize_path(File.join('/stylesheets', css_file))
135
+ tag 'link', 'href' => href, 'type' => 'text/css', 'rel' => 'stylesheet', 'media' => media
136
+ end
137
+
138
+ def tag(*args)
139
+ name = args.shift
140
+ options = args.extract_options!
141
+ open = args.shift
142
+ "<#{name}#{tag_attrs(options)}#{open ? '>' : '/>'}"
143
+ end
144
+
145
+ def tag_attrs(attrs)
146
+ attrs.inject('') do |result, (key, value)|
147
+ result + " #{key}=\"#{h(value)}\""
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,10 @@
1
+ module Ichiban
2
+ module Layouts
3
+ def wrap_in_layouts(erb_context, html)
4
+ erb_context.layout_stack.reverse.inject(html) do |html, layout_name|
5
+ layout_path = Path.new(File.join(Ichiban.project_root, 'layouts', layout_name + '.html'))
6
+ ErbPage.new(File.read(layout_path.abs), :filename => layout_path.abs).evaluate(erb_context) { html }
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Ichiban
2
+ class Compiler
3
+ def load_all_ruby_files
4
+ models.each { |path| load path.abs }
5
+ helpers.each { |path| load_helper path }
6
+ end
7
+
8
+ def load_helper(path)
9
+ load path.abs
10
+ mod_name = path.filename_without_ext.classify
11
+ begin
12
+ mod = Object.const_get(mod_name)
13
+ rescue NameError
14
+ raise "Expected #{path.abs} to define module #{classname}"
15
+ end
16
+ ErbPage::Context.send(:include, mod)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ module Ichiban
2
+ def self.logger
3
+ @logger ||= Logger.new
4
+ end
5
+
6
+ class Logger
7
+ def compilation(src, dst)
8
+ out "#{src} => #{dst}"
9
+ end
10
+
11
+ def exception(exc)
12
+ out "#{exc.class.to_s}: #{exc.message}\n" + exc.backtrace.collect { |line| ' ' + line }.join("\n")
13
+ end
14
+
15
+ def out(msg)
16
+ puts msg
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ module Ichiban
2
+ class Compiler
3
+ def content_dest(src_path)
4
+ if src_path.ext == 'markdown'
5
+ src_path = src_path.replace_ext('html')
6
+ end
7
+ Path.new(
8
+ File.join(Ichiban.project_root, 'compiled',
9
+ src_path.relative_from('content')
10
+ )
11
+ )
12
+ end
13
+
14
+ def image_dest(src_path)
15
+ Path.new(
16
+ File.join(Ichiban.project_root, 'compiled', 'images',
17
+ src_path.relative_from('images')
18
+ )
19
+ )
20
+ end
21
+
22
+ def javascript_dest(src_path)
23
+ Path.new(
24
+ File.join(Ichiban.project_root, 'compiled', 'javascripts',
25
+ src_path.relative_from('javascripts')
26
+ )
27
+ )
28
+ end
29
+
30
+ def stylesheet_dest(src_path)
31
+ Path.new(
32
+ File.join(Ichiban.project_root, 'compiled', 'stylesheets',
33
+ src_path.relative_from('stylesheets')
34
+ )
35
+ ).replace_ext('css')
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ # A Path object is a wrapper around an absolute path.
2
+
3
+ class Path
4
+ attr_reader :abs
5
+
6
+ def directory?
7
+ File.directory?(abs)
8
+ end
9
+
10
+ def dirname
11
+ File.dirname(abs)
12
+ end
13
+
14
+ def ext
15
+ File.extname(abs).slice(1..-1)
16
+ end
17
+
18
+ def filename
19
+ File.basename(abs)
20
+ end
21
+
22
+ def filename_without_ext
23
+ File.basename(abs, File.extname(abs))
24
+ end
25
+
26
+ def initialize(abs)
27
+ @abs = abs
28
+ end
29
+
30
+ # Returns a string relative to the given folder
31
+ def relative_from(folder_in_project_root)
32
+ prefix = File.join(Ichiban.project_root, folder_in_project_root)
33
+ raise(ArgumentError, "#{abs} does not start with #{prefix}") unless abs.start_with?(prefix)
34
+ abs.slice(prefix.length..-1)
35
+ end
36
+
37
+ def replace_ext(new_ext)
38
+ self.class.new(abs.sub(/\..+$/, '.' + new_ext))
39
+ end
40
+
41
+ # Only meaningful for paths in the compiled directory. Returns a string representing the path from the web root.
42
+ # Assumes Ichiban's standard URL rewriting rules are in effect.
43
+ def web
44
+ web_path = relative_from('compiled')
45
+ if web_path.end_with?('.html')
46
+ if web_path.end_with?('index.html')
47
+ web_path.slice(0..-11) # Slice index.html off the end
48
+ else
49
+ web_path.slice(0..-6) + '/' # Slice .html off the end and add the trailing slash
50
+ end
51
+ else
52
+ web_path
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,33 @@
1
+ module Ichiban
2
+ # This class runs .rb files located in the content folder. Its main purpose is to generate pages from databases.
3
+ class ScriptRunner
4
+ include Layouts
5
+
6
+ def generate(template_path_from_content_root, destination_path_from_site_root, ivars)
7
+ template_path = Path.new(
8
+ File.join(Ichiban.project_root, 'content', template_path_from_content_root)
9
+ )
10
+ destination_path = Path.new(
11
+ File.join(Ichiban.project_root, 'compiled', destination_path_from_site_root + '.html')
12
+ )
13
+ erb_context = ErbPage::Context.new(ivars.merge(:_current_path => destination_path_from_site_root))
14
+ result = ErbPage.new(File.read(template_path.abs), :filename => template_path.abs).evaluate(erb_context)
15
+ result = wrap_in_layouts(erb_context, result)
16
+ File.open(destination_path.abs, 'w') do |f|
17
+ f.write result
18
+ end
19
+ end
20
+
21
+ def initialize(path)
22
+ @source = path.abs
23
+ end
24
+
25
+ def self.run_script_file(path)
26
+ new(path).run
27
+ end
28
+
29
+ def run
30
+ instance_eval(File.read(@source), @source)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module Ichiban
2
+ def self.define_tasks(rakefile)
3
+ desc 'Generates the static site and puts it in the "compiled" directory. Files will only be compiled as necessary. If a layout has been modified, all files will be recompiled.'
4
+ task :compile => :init do
5
+ Ichiban::Compiler.new.compile
6
+ end
7
+
8
+ desc 'Generates the static site and puts it in the "compiled" directory. All files will be compiled from scratch.'
9
+ task :fresh => :init do
10
+ Ichiban::Compiler.new.fresh
11
+ end
12
+
13
+ desc 'Continuously watch for changes and compile files as needed.'
14
+ task :watch => :init do
15
+ Ichiban::Watcher.new.watch
16
+ end
17
+
18
+ desc 'Initialize Ichiban.'
19
+ task :init do
20
+ Ichiban.configure_for_project(File.dirname(File.expand_path(rakefile)))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Ichiban
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,7 @@
1
+ module Ichiban
2
+ class Watcher
3
+ def watch
4
+ raise 'TODO'
5
+ end
6
+ end
7
+ end
data/lib/ichiban.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'csv'
2
+ require 'fileutils'
3
+ require 'active_support/core_ext/class/attribute'
4
+ require 'active_support/core_ext/object/blank'
5
+ require 'active_support/inflector'
6
+ require 'erubis'
7
+ require 'maruku'
8
+ require 'sass'
9
+
10
+ # Order matters!
11
+ require 'ichiban/command'
12
+ require 'ichiban/layouts'
13
+ require 'ichiban/helpers'
14
+ require 'ichiban/compilation'
15
+ require 'ichiban/config'
16
+ require 'ichiban/erb_page'
17
+ require 'ichiban/files'
18
+ require 'ichiban/logger'
19
+ require 'ichiban/loading'
20
+ require 'ichiban/mapping'
21
+ require 'ichiban/path'
22
+ require 'ichiban/script_runner'
23
+ require 'ichiban/tasks'
24
+ require 'ichiban/version'
25
+ require 'ichiban/watcher'
data/sample/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'ichiban'
2
+ Ichiban.define_tasks(__FILE__)
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <h1>About</h1>
10
+
11
+ </body>
12
+
13
+ </html>
File without changes
Binary file
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <h1>Home</h1>
10
+
11
+ </body>
12
+
13
+ </html>
@@ -0,0 +1 @@
1
+ // Foo
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <h1> </h1>
10
+
11
+ </body>
12
+
13
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <h1>Andre Marques</h1>
10
+
11
+ </body>
12
+
13
+ </html>
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <h1>Employees</h1>
10
+ <ul>
11
+ <li><a href="/staff/jarrett-{employee.last.downcase}/">Jarrett Colby</a></li>
12
+ <li><a href="/staff/andre-{employee.last.downcase}/">Andre Marques</a></li>
13
+ </ul>
14
+
15
+ </body>
16
+
17
+ </html>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <h1>Jarrett Colby</h1>
10
+
11
+ </body>
12
+
13
+ </html>
@@ -0,0 +1 @@
1
+ h1, h2, h3, h4, h5, h6 {font-size: 100%;}
@@ -0,0 +1 @@
1
+ body h1{color:#f04040}
data/sample/config.rb ADDED
@@ -0,0 +1,3 @@
1
+ Ichiban.config do |cfg|
2
+ cfg.relative_url_root = '/'
3
+ end
@@ -0,0 +1 @@
1
+ <h1>About</h1>
@@ -0,0 +1,3 @@
1
+ <!-- This page demonstrates what happens when an error is raised inside a page. The compiler should report the error and gracefully recover. -->
2
+
3
+ <% raise "This error is being generated by content/bad.html. This is intentional. The purpose is to illustrate how the compiler recovers gracefully from an in-page error." %>
@@ -0,0 +1 @@
1
+ <h1>Home</h1>
@@ -0,0 +1 @@
1
+ <h1><%= @first %> <%= @last %></h1>
@@ -0,0 +1,6 @@
1
+ <h1>Employees</h1>
2
+ <ul>
3
+ <% Employee.all.each do |employee| %>
4
+ <li><%= link_to employee.first + ' ' + employee.last, employee_path(employee) %></li>
5
+ <% end %>
6
+ </ul>
@@ -0,0 +1,2 @@
1
+ Jarrett,Colby
2
+ Andre,Marques
@@ -0,0 +1 @@
1
+ <h1>The page does not exist.</h1>
@@ -0,0 +1,5 @@
1
+ module StaffHelper
2
+ def employee_path(employee)
3
+ "/staff/#{employee.first.downcase}-{employee.last.downcase}/"
4
+ end
5
+ end
Binary file
@@ -0,0 +1 @@
1
+ // Foo
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE HTML>
2
+ <html>
3
+ <head>
4
+ <title>Sample</title>
5
+ </head>
6
+
7
+ <body>
8
+
9
+ <%= yield %>
10
+
11
+ </body>
12
+
13
+ </html>
@@ -0,0 +1,16 @@
1
+ class Employee
2
+ def self.all
3
+ CSV.read(File.join(Ichiban.project_root, 'data', 'employees.csv')).collect do |row|
4
+ new(row[0], row[1])
5
+ end
6
+ end
7
+
8
+ attr_accessor :first
9
+
10
+ def initialize(first, last)
11
+ @first = first
12
+ @last = last
13
+ end
14
+
15
+ attr_accessor :last
16
+ end
@@ -0,0 +1 @@
1
+ raise 'This error is being generated by scripts/bad.rb. This is intentional. The purpose is to illustrate how the compiler recovers gracefully from a script error.'
@@ -0,0 +1,8 @@
1
+ Employee.all.each do |employee|
2
+ generate(
3
+ 'staff/_employee.html',
4
+ File.join('staff', "#{employee.first.downcase}-#{employee.last.downcase}"),
5
+ :first => employee.first,
6
+ :last => employee.last
7
+ )
8
+ end
@@ -0,0 +1 @@
1
+ h1, h2, h3, h4, h5, h6 {font-size: 100%;}
@@ -0,0 +1,5 @@
1
+ $red: #f04040;
2
+
3
+ body {
4
+ h1 {color: $red;}
5
+ }
@@ -0,0 +1,89 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'spec_helper')
2
+ require 'nokogiri'
3
+
4
+ describe Ichiban do
5
+ def compiled(path = nil)
6
+ path.nil? ? File.join(root, 'compiled') : File.join(root, 'compiled', path)
7
+ end
8
+
9
+ def parse_compiled(path)
10
+ @parsed_html ||= {}
11
+ @parsed_html[path] ||= Nokogiri.HTML(File.read(compiled(path)))
12
+ end
13
+
14
+ def root
15
+ File.expand_path(File.join(File.dirname(__FILE__), '../sample'))
16
+ end
17
+
18
+ before(:all) do
19
+ FileUtils.rm_rf compiled
20
+ FileUtils.mkdir compiled
21
+ end
22
+
23
+ shared_examples_for 'compilation' do
24
+ it 'creates all the content files and directories as needed' do
25
+ compiled('index.html').should be_file
26
+ compiled('about.html').should be_file
27
+ compiled('staff/index.html').should be_file
28
+ end
29
+
30
+ it 'renders the content' do
31
+ parse_compiled('index.html').css('h1').inner_html.should == 'Home'
32
+ parse_compiled('about.html').css('h1').inner_html.should == 'About'
33
+ parse_compiled('staff/index.html').css('h1').inner_html.should == 'Employees'
34
+ end
35
+
36
+ it 'inserts the content into the layout' do
37
+ parse_compiled('index.html').css('head').length.should == 1
38
+ end
39
+
40
+ it 'copies images' do
41
+ compiled('images/check.png').should be_file
42
+ end
43
+
44
+ it 'compiles SCSS' do
45
+ File.read(compiled('stylesheets/screen.css')).should include('body h1{color:#f04040}')
46
+ end
47
+
48
+ it 'copies CSS' do
49
+ compiled('stylesheets/reset.css').should be_file
50
+ end
51
+
52
+ it 'copies JS' do
53
+ compiled('javascripts/interaction.js').should be_file
54
+ end
55
+
56
+ it 'logs errors raised within content files' do
57
+ Ichiban.logger.test_messages.detect do |msg|
58
+ msg.start_with?('RuntimeError: This error is being generated by content/bad.html. This is intentional. The purpose is to illustrate how the compiler recovers gracefully from an in-page error.')
59
+ end.should_not be_nil
60
+ end
61
+
62
+ it 'logs errors raised within scripts' do
63
+ Ichiban.logger.test_messages.detect do |msg|
64
+ msg.start_with?('RuntimeError: This error is being generated by scripts/bad.rb. This is intentional. The purpose is to illustrate how the compiler recovers gracefully from a script error.')
65
+ end.should_not be_nil
66
+ end
67
+
68
+ context 'with scripts' do
69
+ it 'generates files' do
70
+ compiled('staff/andre-marques.html').should be_file
71
+ compiled('staff/jarrett-colby.html').should be_file
72
+ end
73
+
74
+ it 'interpolates data into the template' do
75
+ parse_compiled('staff/andre-marques.html').css('h1').inner_html == 'Andre Marques'
76
+ parse_compiled('staff/jarrett-colby.html').css('h1').inner_html == 'Jarrett Colby'
77
+ end
78
+ end
79
+ end
80
+
81
+ context 'fresh compile' do
82
+ before(:all) do
83
+ Ichiban.configure_for_project(root)
84
+ Ichiban::Compiler.new.fresh
85
+ end
86
+
87
+ it_should_behave_like 'compilation'
88
+ end
89
+ end
data/spec/path_spec.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'ichiban'
2
+
3
+ include Ichiban
4
+
5
+ describe Path do
6
+ #describe '#relative_from' do
7
+ # it 'returns the path relative to the given subfolder' do
8
+ # Path.new('/project/content/foo/index.html').relative_from('content').should == 'foo/index.html'
9
+ # end
10
+ #
11
+ # it 'raises if the file is not in the given subfolder' do
12
+ # lambda { Path.new('/project/content/foo/index.html').relative_from('errors') }.should raise_error
13
+ # end
14
+ #end
15
+ end
@@ -0,0 +1,19 @@
1
+ # Put the working directory's version of Ichiban first in the list of load paths. That way,
2
+ # we won't load an installed version of the gem.
3
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__)), '../lib')
4
+
5
+ require 'ichiban'
6
+
7
+ module Ichiban
8
+ class Logger
9
+ def out(msg)
10
+ (@test_messages ||= []) << msg
11
+ end
12
+
13
+ attr_reader :test_messages
14
+ end
15
+ end
16
+
17
+ RSpec::Matchers.define :be_file do
18
+ match { |path| File.exists?(path) }
19
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ichiban
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jarrett Colby
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &2157055820 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2157055820
25
+ - !ruby/object:Gem::Dependency
26
+ name: erubis
27
+ requirement: &2157055400 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2157055400
36
+ - !ruby/object:Gem::Dependency
37
+ name: maruku
38
+ requirement: &2157054980 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *2157054980
47
+ description: The most elegant way to compile static websites
48
+ email:
49
+ - jarrettcolby@gmail.com
50
+ executables:
51
+ - ichiban.rb
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - README
58
+ - bin/ichiban.rb
59
+ - ichiban.gemspec
60
+ - klass.rb
61
+ - lib/ichiban.rb
62
+ - lib/ichiban/command.rb
63
+ - lib/ichiban/compilation.rb
64
+ - lib/ichiban/config.rb
65
+ - lib/ichiban/erb_page.rb
66
+ - lib/ichiban/files.rb
67
+ - lib/ichiban/helpers.rb
68
+ - lib/ichiban/layouts.rb
69
+ - lib/ichiban/loading.rb
70
+ - lib/ichiban/logger.rb
71
+ - lib/ichiban/mapping.rb
72
+ - lib/ichiban/path.rb
73
+ - lib/ichiban/script_runner.rb
74
+ - lib/ichiban/tasks.rb
75
+ - lib/ichiban/version.rb
76
+ - lib/ichiban/watcher.rb
77
+ - sample/Rakefile
78
+ - sample/compiled/about.html
79
+ - sample/compiled/bad.html
80
+ - sample/compiled/images/check.png
81
+ - sample/compiled/index.html
82
+ - sample/compiled/javascripts/interaction.js
83
+ - sample/compiled/staff/_employee.html
84
+ - sample/compiled/staff/andre-marques.html
85
+ - sample/compiled/staff/index.html
86
+ - sample/compiled/staff/jarrett-colby.html
87
+ - sample/compiled/stylesheets/reset.css
88
+ - sample/compiled/stylesheets/screen.css
89
+ - sample/config.rb
90
+ - sample/content/about.html
91
+ - sample/content/bad.html
92
+ - sample/content/index.html
93
+ - sample/content/staff/_employee.html
94
+ - sample/content/staff/index.html
95
+ - sample/data/employees.csv
96
+ - sample/errors/404.html
97
+ - sample/helpers/staff_helper.rb
98
+ - sample/images/check.png
99
+ - sample/javascripts/interaction.js
100
+ - sample/layouts/default.html
101
+ - sample/models/employee.rb
102
+ - sample/scripts/bad.rb
103
+ - sample/scripts/staff.rb
104
+ - sample/stylesheets/reset.css
105
+ - sample/stylesheets/screen.scss
106
+ - spec/integration_spec.rb
107
+ - spec/path_spec.rb
108
+ - spec/spec_helper.rb
109
+ homepage: ''
110
+ licenses: []
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ requirements: []
128
+ rubyforge_project: ichiban
129
+ rubygems_version: 1.8.11
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: Static website compiler
133
+ test_files:
134
+ - spec/integration_spec.rb
135
+ - spec/path_spec.rb
136
+ - spec/spec_helper.rb