ichiban 0.0.2

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