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