ichiban 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README +56 -0
- data/bin/ichiban.rb +6 -0
- data/ichiban.gemspec +25 -0
- data/klass.rb +7 -0
- data/lib/ichiban/command.rb +11 -0
- data/lib/ichiban/compilation.rb +93 -0
- data/lib/ichiban/config.rb +31 -0
- data/lib/ichiban/erb_page.rb +16 -0
- data/lib/ichiban/files.rb +105 -0
- data/lib/ichiban/helpers.rb +151 -0
- data/lib/ichiban/layouts.rb +10 -0
- data/lib/ichiban/loading.rb +19 -0
- data/lib/ichiban/logger.rb +19 -0
- data/lib/ichiban/mapping.rb +38 -0
- data/lib/ichiban/path.rb +55 -0
- data/lib/ichiban/script_runner.rb +33 -0
- data/lib/ichiban/tasks.rb +23 -0
- data/lib/ichiban/version.rb +3 -0
- data/lib/ichiban/watcher.rb +7 -0
- data/lib/ichiban.rb +25 -0
- data/sample/Rakefile +2 -0
- data/sample/compiled/about.html +13 -0
- data/sample/compiled/bad.html +0 -0
- data/sample/compiled/images/check.png +0 -0
- data/sample/compiled/index.html +13 -0
- data/sample/compiled/javascripts/interaction.js +1 -0
- data/sample/compiled/staff/_employee.html +13 -0
- data/sample/compiled/staff/andre-marques.html +13 -0
- data/sample/compiled/staff/index.html +17 -0
- data/sample/compiled/staff/jarrett-colby.html +13 -0
- data/sample/compiled/stylesheets/reset.css +1 -0
- data/sample/compiled/stylesheets/screen.css +1 -0
- data/sample/config.rb +3 -0
- data/sample/content/about.html +1 -0
- data/sample/content/bad.html +3 -0
- data/sample/content/index.html +1 -0
- data/sample/content/staff/_employee.html +1 -0
- data/sample/content/staff/index.html +6 -0
- data/sample/data/employees.csv +2 -0
- data/sample/errors/404.html +1 -0
- data/sample/helpers/staff_helper.rb +5 -0
- data/sample/images/check.png +0 -0
- data/sample/javascripts/interaction.js +1 -0
- data/sample/layouts/default.html +13 -0
- data/sample/models/employee.rb +16 -0
- data/sample/scripts/bad.rb +1 -0
- data/sample/scripts/staff.rb +8 -0
- data/sample/stylesheets/reset.css +1 -0
- data/sample/stylesheets/screen.scss +5 -0
- data/spec/integration_spec.rb +89 -0
- data/spec/path_spec.rb +15 -0
- data/spec/spec_helper.rb +19 -0
- metadata +136 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
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,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
|
data/lib/ichiban/path.rb
ADDED
@@ -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
|
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
File without changes
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
// Foo
|
@@ -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 @@
|
|
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 @@
|
|
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 @@
|
|
1
|
+
<h1>The page does not exist.</h1>
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
// Foo
|
@@ -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 @@
|
|
1
|
+
h1, h2, h3, h4, h5, h6 {font-size: 100%;}
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|