ichiban 1.0.0 → 1.0.6

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/bin/ichiban ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # for dev purposes:
4
+ #$:.unshift File.expand_path(File.join(File.dirname(__FILE__), '../lib'))
5
+
6
+ require 'rubygems'
7
+ require 'ichiban'
8
+
9
+ Ichiban::Command.new(ARGV).run
@@ -0,0 +1,19 @@
1
+ load 'deploy'
2
+
3
+ set :scm, :git # Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none`
4
+ set :repository, 'ssh://user@example.com/my_site.git'
5
+ set :branch, 'master'
6
+ set :keep_releases, 2
7
+ set :deploy_via, :remote_cache
8
+
9
+ role :web, 'example.com'
10
+ set :user, 'username'
11
+ set :use_sudo, false
12
+ set :deploy_to, '/home/username/my_site'
13
+
14
+ namespace :deploy do
15
+ task(:restart) {}
16
+ end
17
+
18
+ set :keep_releases, 2
19
+ after "deploy:update", "deploy:cleanup"
@@ -0,0 +1 @@
1
+ body {font-family: sans-serif;}
@@ -0,0 +1 @@
1
+ // Nothing here yet. This JS file is referenced from the default layout.
@@ -0,0 +1,6 @@
1
+ Put PDFs, PPTs, and whatever other miscellaneous asset files here. Paths in this folder map to paths
2
+ in the compiled folder. For example, assets/about_us.pdf would be copied to compiled/about_us.pdf.
3
+
4
+ Remember that everything you put here will be checked into git. If you're deploying with Capistrano,
5
+ a large repository will slow down your deployment. For this reason, and for better download times
6
+ for your users, it's often better to host your large asset files on a CDN instead.
@@ -0,0 +1 @@
1
+ body {font-family: sans-serif;}
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Home - Site Name Here</title>
5
+ <link rel="stylesheet" type="text/css" href="/css/screen.css"/>
6
+ <script type="text/javascript" src="/js/interaction.js"></script>
7
+ </head>
8
+
9
+ <body>
10
+ <h1>Home</h1>
11
+ </body>
12
+ </html>
@@ -0,0 +1 @@
1
+ // Nothing here yet. This JS file is referenced from the default layout.
@@ -0,0 +1,3 @@
1
+ Ichiban.config do |config|
2
+ config.relative_url_root = '/'
3
+ end
@@ -0,0 +1,2 @@
1
+ If you have some data sources that you use to auto-generate HTML, such as CSV or XML files,
2
+ you can put them in here.
@@ -0,0 +1,10 @@
1
+ You can write custom helper modules and put them in this directory. The methods they define
2
+ will be available in your templates. For example:
3
+
4
+ # helpers/my_helper.rb
5
+
6
+ module MyHelper
7
+ def double(num)
8
+ num * 2
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ <% @page_title = 'Home' %>
2
+
3
+ <h1>Home</h1>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= @page_title %> - Site Name Here</title>
5
+ <%= stylesheet_link_tag 'screen.css' %>
6
+ <%= javascript_include_tag 'interaction.js' %>
7
+ </head>
8
+
9
+ <body>
10
+ <%= yield %>
11
+ </body>
12
+ </html>
@@ -0,0 +1,3 @@
1
+ IF you're doing a lot with datasources, it's good to keep things tidy and put the logic in
2
+ separate model files. That's what this directory is for. Any Ruby files in this directory will be
3
+ automatically loaded.
@@ -0,0 +1,6 @@
1
+ This folder is for scripts that generate pages.
2
+
3
+ If you want some auto-generated code *within a single page*, it's easy: Just use some <% %> tags.
4
+ But suppose you have an XML file describing every employee in your company, and you want an
5
+ *individual* page for each one. That's where this folder coms in. You need to create a script
6
+ that generates each page based on a template. See the documentation for more details.
@@ -0,0 +1,24 @@
1
+ RewriteEngine On
2
+
3
+ # Force the trailing slash for all pages unless they end in .html
4
+ RewriteCond %{REQUEST_FILENAME} !-f
5
+ RewriteCond %{REQUEST_FILENAME} !-d
6
+ # Skipping pages with the .html extension is necessary to avoid an infinite chain of .html. See the note at the bottom.
7
+ RewriteCond %{REQUEST_URI} !\.html$
8
+ RewriteRule ^(.*[^/])$ /$1/ [R=301,L]
9
+
10
+ RewriteCond %{REQUEST_FILENAME} !-f
11
+ RewriteCond %{REQUEST_FILENAME} !-d
12
+ # If the HTML file doesn't exist don't attempt to rewrite, or we'll get an infinite loop
13
+ RewriteCond %{REQUEST_FILENAME}.html -f
14
+ # If we've already tried appending HTML and the file still doesn't exist, don't do it again. See the note at the bottom.
15
+ RewriteCond %{REQUEST_URI} !\.html$
16
+ RewriteRule ^(.*)/$ $1.html
17
+
18
+ ErrorDocument 404 /404.html
19
+ ErrorDocument 422 /422.html
20
+ ErrorDocument 500 /500.html
21
+
22
+ # Note on infinite .html appending loops: We have to take pains to ensure that if a certain file, say foo.html, exists,
23
+ # then a path like /foo/bar won't get .html appended an infinite number of times. This can happen because %{REQUEST_FILENAME}
24
+ # would match /foo/bar/ to foo.html. Our redirect to force trailing slashes then sends us through an infinite loop.
@@ -0,0 +1,21 @@
1
+ module Ichiban
2
+ class AssetCompiler
3
+ def initialize(file)
4
+ @file = file
5
+ end
6
+
7
+ def compile
8
+ dir = File.dirname @file.dest
9
+ unless File.directory? dir
10
+ FileUtils.mkdir_p dir
11
+ end
12
+ case @file
13
+ when Ichiban::SCSSFile
14
+ Sass.compile_file @file.abs, @file.dest
15
+ else
16
+ FileUtils.cp @file.abs, @file.dest
17
+ end
18
+ Ichiban.logger.compilation(@file.abs, @file.dest)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module Ichiban
2
+ def self.load_bundle
3
+ if Ichiban.project_root and File.exists?(File.join(Ichiban.project_root, 'Gemfile'))
4
+ Bundler.require
5
+ end
6
+ end
7
+ end
@@ -6,15 +6,25 @@ module Ichiban
6
6
  end
7
7
 
8
8
  def print_usage
9
- puts "Usage: ichiban <command>"
10
- puts " Available commands: watch"
9
+ puts(
10
+ "Usage: ichiban [command]\n" +
11
+ "Available commands: \n" +
12
+ " watch\n" +
13
+ " new [path]"
14
+ )
11
15
  end
12
16
 
13
17
  def run
14
- Ichiban.project_root = Dir.getwd
15
18
  case @task
16
19
  when 'watch'
20
+ Ichiban.project_root = Dir.getwd
21
+ Ichiban.load_bundle
17
22
  Ichiban::Watcher.new.start
23
+ when 'new'
24
+ Ichiban::ProjectGenerator.new(
25
+ File.expand_path(@args[0])
26
+ ).generate
27
+ puts "Initialized Ichiban project in #{@args[0]}"
18
28
  else
19
29
  print_usage
20
30
  end
@@ -9,8 +9,8 @@ module Ichiban
9
9
  attr_writer :relative_url_root
10
10
 
11
11
  def self.load_file
12
- config_file = ::File.join(Ichiban.project_root, 'config.rb')
13
- raise "#{config_file} must exist" unless ::File.exists?(config_file)
12
+ config_file = File.join(Ichiban.project_root, 'config.rb')
13
+ raise "#{config_file} must exist" unless File.exists?(config_file)
14
14
  load config_file
15
15
  end
16
16
 
@@ -2,19 +2,16 @@ module Ichiban
2
2
  class Deleter
3
3
  # Deletes a file's associated destination file, if any.
4
4
  def delete_dest(path)
5
- file = Ichiban::File.from_abs(path)
6
- # file will be nil if the path doesn't map to a known subclass of Ichiban::File. Furthermore,
7
- # even if file is not nil, it may be a kind of Ichiban::File that does not have a destination.
5
+ file = Ichiban::ProjectFile.from_abs(path)
6
+ # file will be nil if the path doesn't map to a known subclass of IchibanFile. Furthermore,
7
+ # even if file is not nil, it may be a kind of IchibanFile that does not have a destination.
8
8
  if file and file.has_dest?
9
9
  dest = file.dest
10
10
  else
11
11
  dest = nil
12
12
  end
13
- if dest and ::File.exists?(dest)
14
- puts 'yep'
13
+ if dest and File.exists?(dest)
15
14
  FileUtils.rm(dest)
16
- else
17
- puts 'nope'
18
15
  end
19
16
 
20
17
  # Log the deletion(s)
@@ -2,32 +2,53 @@ module Ichiban
2
2
  module Dependencies
3
3
  @graphs = {}
4
4
 
5
- # graph_file_path is an absolute path.
5
+ # Does not delete the files. Just clears the graphs from memory. This gets called whenever
6
+ # Ichiban.project_root is changed.
7
+ def self.clear_graphs
8
+ @graphs = {}
9
+ end
10
+
11
+ def self.delete_dep(graph_file_path, dep)
12
+ ensure_graph_initialized(graph_file_path)
13
+ graph = @graphs[graph_file_path]
14
+ graph.each do |ind, deps|
15
+ deps.delete dep
16
+ end
17
+ save_graph_file(graph_file_path, graph)
18
+ end
19
+
20
+ # graph_file_path is a relative path
6
21
  def self.graph(graph_file_path)
7
22
  ensure_graph_initialized(graph_file_path)
8
23
  @graphs[graph_file_path]
9
24
  end
10
25
 
26
+ # graph_file_path is a relative path
11
27
  def self.ensure_graph_initialized(graph_file_path)
12
28
  unless @graphs[graph_file_path]
13
- if ::File.exists?(graph_file_path)
14
- @graphs[graph_file_path] = JSON.parse(::File.read(graph_file_path))
29
+ abs = File.join(Ichiban.project_root, graph_file_path)
30
+ if File.exists?(abs)
31
+ @graphs[graph_file_path] = JSON.parse(File.read(abs))
15
32
  else
16
33
  @graphs[graph_file_path] = {}
17
34
  end
18
35
  end
19
36
  end
20
37
 
38
+ def self.save_graph_file(graph_file_path, graph)
39
+ File.open(File.join(Ichiban.project_root, graph_file_path), 'w') do |f|
40
+ f << JSON.generate(graph)
41
+ end
42
+ end
43
+
21
44
  # Loads the graph from disk if it's not already in memory. Updates the graph. Writes the new
22
- # graph to disk. graph_file_path is an absolute path.
45
+ # graph to disk. graph_file_path is a relative path.
23
46
  def self.update(graph_file_path, ind, dep)
24
47
  ensure_graph_initialized(graph_file_path)
25
48
  graph = @graphs[graph_file_path]
26
49
  graph[ind] ||= []
27
50
  graph[ind] << dep unless graph[ind].include?(dep)
28
- ::File.open(graph_file_path, 'w') do |f|
29
- f << JSON.generate(graph)
30
- end
51
+ save_graph_file(graph_file_path, graph)
31
52
  end
32
53
  end
33
54
  end
data/lib/ichiban/file.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  module Ichiban
2
- class File
2
+ class ProjectFile
3
3
  attr_reader :abs
4
4
 
5
5
  # Returns an absolute path in the compiled directory
6
6
  def dest
7
- ::File.join(Ichiban.project_root, 'compiled', dest_rel_to_compiled)
7
+ File.join(Ichiban.project_root, 'compiled', dest_rel_to_compiled)
8
8
  end
9
9
 
10
10
  # Returns a new instance based on an absolute path. Will automatically pick the right subclass.
@@ -12,10 +12,14 @@ module Ichiban
12
12
  def self.from_abs(abs)
13
13
  rel = abs.slice(Ichiban.project_root.length..-1) # Relative to project root
14
14
  rel.sub!(/^\//, '') # Remove leading slash
15
- if rel.start_with?('html') and rel.end_with?('.html')
16
- Ichiban::HTMLFile.new(rel)
15
+ if rel.start_with?('html') and (rel.end_with?('.html') or rel.end_with?('.md') or rel.end_with?('.markdown'))
16
+ if File.basename(rel).start_with?('_')
17
+ Ichiban::PartialHTMLFile.new(rel)
18
+ else
19
+ Ichiban::HTMLFile.new(rel)
20
+ end
17
21
  elsif rel.start_with?('layouts') and rel.end_with?('.html')
18
- Ichiban::LayoutFIle.new(rel)
22
+ Ichiban::LayoutFile.new(rel)
19
23
  elsif rel.start_with?('assets/js')
20
24
  Ichiban::JSFile.new(rel)
21
25
  elsif rel.start_with?('assets/css') and rel.end_with?('.css')
@@ -34,6 +38,8 @@ module Ichiban
34
38
  Ichiban::ScriptFile.new(rel)
35
39
  elsif rel.start_with?('helpers')
36
40
  Ichiban::HelperFile.new(rel)
41
+ elsif rel == 'webserver/htaccess.txt'
42
+ Ichiban::HtaccessFile.new(rel)
37
43
  else
38
44
  nil
39
45
  end
@@ -45,16 +51,18 @@ module Ichiban
45
51
 
46
52
  def initialize(rel)
47
53
  @rel = rel
48
- @abs = ::File.join(Ichiban.project_root, rel)
54
+ @abs = File.join(Ichiban.project_root, rel)
49
55
  end
50
56
 
57
+ attr_reader :rel
58
+
51
59
  # Returns a new path where the old extension is replaced with new_ext
52
60
  def replace_ext(path, new_ext)
53
61
  path.sub(/\..+$/, '.' + new_ext)
54
62
  end
55
63
  end
56
64
 
57
- class HTMLFile < File
65
+ class HTMLFile < ProjectFile
58
66
  def dest_rel_to_compiled
59
67
  d = @rel.slice('html/'.length..-1)
60
68
  (d.end_with?('.markdown') or d.end_with?('.md')) ? replace_ext(d, 'html') : d
@@ -63,53 +71,150 @@ module Ichiban
63
71
  def update
64
72
  Ichiban::HTMLCompiler.new(self).compile
65
73
  end
74
+
75
+ def web_path
76
+ d = dest_rel_to_compiled
77
+ '/' + File.basename(d, File.extname(d)) + '/'
78
+ end
79
+ end
80
+
81
+ class PartialHTMLFile < ProjectFile
82
+ # Returns something like 'foo/bar'
83
+ def partial_name
84
+ File.basename(
85
+ @abs.slice(Ichiban.project_root.length + 1..-1),
86
+ File.extname(@abs)
87
+ )
88
+ end
89
+
90
+ def update
91
+ # Normal HTML files that depend on this partial
92
+ if deps = Ichiban::Dependencies.graph('.partial_dependencies.json')[partial_name]
93
+ deps.each do |dep|
94
+ # dep will be a path relative to the html directory. It looks like this: 'folder/file.html'
95
+ Ichiban::HTMLFile.new(File.join('html', dep)).update
96
+ end
97
+ end
98
+
99
+ # Scripts that depend on this partial
100
+ dep_key = "html/#{partial_name}.html"
101
+ if deps = Ichiban::Dependencies.graph('.script_dependencies.json')[dep_key]
102
+ deps.each do |dep|
103
+ # dep will be a path relative to the html directory. It looks like this: 'folder/file.html'
104
+ script_path = File.join(Ichiban.project_root, dep)
105
+ Ichiban.logger.script_run(@abs, script_path)
106
+ script = Ichiban::Script.new(script_path).run
107
+ end
108
+ end
109
+ end
66
110
  end
67
111
 
68
- class LayoutFile < File
112
+ class LayoutFile < ProjectFile
113
+ def layout_name
114
+ File.basename(@abs, File.extname(@abs))
115
+ end
116
+
117
+ def update
118
+ Ichiban.logger.layout(@abs)
119
+ if deps = Ichiban::Dependencies.graph('.layout_dependencies.json')[layout_name]
120
+ deps.each do |dep|
121
+ # dep is a path relative to the project root
122
+ if File.exists?(File.join(Ichiban.project_root, dep))
123
+ Ichiban::HTMLFile.new(dep).update
124
+ else
125
+ Dependencies.delete_dep('.layout_dependencies.json', dep)
126
+ end
127
+ end
128
+ end
129
+ end
69
130
  end
70
131
 
71
- class JSFile < File
132
+ class JSFile < ProjectFile
72
133
  def dest_rel_to_compiled
73
134
  File.join('js', @rel.slice('assets/js/'.length..-1))
74
135
  end
136
+
137
+ def update
138
+ Ichiban::AssetCompiler.new(self).compile
139
+ end
75
140
  end
76
141
 
77
- class CSSFile < File
142
+ class CSSFile < ProjectFile
78
143
  def dest_rel_to_compiled
79
144
  File.join('css', @rel.slice('assets/css/'.length..-1))
80
145
  end
146
+
147
+ def update
148
+ Ichiban::AssetCompiler.new(self).compile
149
+ end
81
150
  end
82
151
 
83
- class SCSSFile < File
152
+ class SCSSFile < ProjectFile
84
153
  def dest_rel_to_compiled
85
154
  replace_ext(
86
155
  File.join('css', @rel.slice('assets/css/'.length..-1)),
87
156
  'css'
88
157
  )
89
158
  end
159
+
160
+ def update
161
+ Ichiban::AssetCompiler.new(self).compile
162
+ end
90
163
  end
91
164
 
92
- class ImageFile < File
165
+ class ImageFile < ProjectFile
93
166
  def dest_rel_to_compiled
94
167
  File.join('img', @rel.slice('assets/img/'.length..-1))
95
168
  end
169
+
170
+ def update
171
+ Ichiban::AssetCompiler.new(self).compile
172
+ end
96
173
  end
97
174
 
98
- class MiscAssetFile < File
175
+ class MiscAssetFile < ProjectFile
99
176
  def dest_rel_to_compiled
100
177
  @rel.slice('assets/misc/'.length..-1)
101
178
  end
179
+
180
+ def update
181
+ Ichiban::AssetCompiler.new(self).compile
182
+ end
102
183
  end
103
184
 
104
- class ModelFile < File
185
+ class HtaccessFile < ProjectFile
186
+ def dest_rel_to_compiled
187
+ '.htaccess'
188
+ end
189
+
190
+ def update
191
+ Ichiban::AssetCompiler.new(self).compile
192
+ end
105
193
  end
106
194
 
107
- class DataFile < File
195
+ class ModelFile < ProjectFile
196
+ def update
197
+ # No-op. The watcher hands the path to each changed model file to the Loader instance.
198
+ # So we don't have to worry about that here.
199
+ end
108
200
  end
109
201
 
110
- class ScriptFile < File
202
+ class HelperFile < ProjectFile
203
+ def update
204
+ # No-op. The watcher hands the path to each changed model file to the Loader instance.
205
+ # So we don't have to worry about that here.
206
+ end
111
207
  end
112
208
 
113
- class HelperFile < File
209
+ class DataFile < ProjectFile
210
+ def update
211
+ Ichiban.script_runner.data_file_changed(@abs)
212
+ end
213
+ end
214
+
215
+ class ScriptFile < ProjectFile
216
+ def update
217
+ Ichiban.script_runner.script_file_changed(@abs)
218
+ end
114
219
  end
115
220
  end
@@ -21,14 +21,14 @@ module Ichiban
21
21
  output << tag_attrs(options) << ">#{content}</#{name}>"
22
22
  end
23
23
 
24
- # Returns the path relative to site root
24
+ # Returns the path relative to site root. Includes leading and trailing slash.
25
25
  def current_path
26
26
  @_current_path
27
27
  end
28
28
 
29
29
  def javascript_include_tag(js_file)
30
30
  js_file = js_file + '.js' unless js_file.end_with?('.js')
31
- path = normalize_path(::File.join('/javascripts', js_file))
31
+ path = normalize_path(File.join('/js', js_file))
32
32
  content_tag 'script', 'type' => 'text/javascript', 'src' => path
33
33
  end
34
34
 
@@ -62,58 +62,16 @@ module Ichiban
62
62
  content_tag 'a', text, options.merge('href' => url)
63
63
  end
64
64
 
65
- # Pass in an array of this form:
66
- #
67
- # [
68
- # ['Link Text', '/path/from/relative/url/root/']
69
- # ['Link Text', '/path/from/relative/url/root/', {'id' => 'foo'}]
70
- # ]
71
- #
72
- # You can also do this, as a convenience:
73
- #
74
- # nav([
75
- # ['Link Text', 'path/from/section/root/']
76
- # ], :section => 'section-name')
77
- #
78
- # Which will generate this href:
79
- #
80
- # /section-name/path/from/section/root/
81
- #
82
- # If you don't specify a section, and your URLs don't have leading slashes,
83
- # the hrefs will use relative URLs.
84
- def nav(items, options = {})
85
- ul_options = _limit_options(options, %w(id class)) { |key, value| key.to_s.start_with?('data-') }
86
- content_tag('ul', ul_options) do
87
- items.inject('') do |lis, (text, path, attrs)|
88
- if options[:section]
89
- path = ::File.join(options[:section], path)
90
- end
91
- path = normalize_path(path)
92
- lis + content_tag('li', (attrs or {})) do
93
- if path_with_slashes(current_path) == path_with_slashes(path)
94
- content_tag('span', text, 'class' => 'selected')
95
- else
96
- link_to(text, path)
97
- end
98
- end
99
- end
100
- end
101
- end
102
-
103
- # If the path has a trailing slash, it will be made absolute using relative_url_root.
65
+ # If the path has a leading slash, it will be made absolute using relative_url_root.
104
66
  # Otherwise, it will remain relative.
105
67
  def normalize_path(path)
106
68
  if path.start_with?('/')
107
- ::File.join(relative_url_root, path)
69
+ File.join(relative_url_root, path)
108
70
  else
109
71
  path
110
72
  end
111
73
  end
112
74
 
113
- def page_title(title)
114
- @page_title = title
115
- end
116
-
117
75
  # Adds leading and trailing slashes if none are present
118
76
  def path_with_slashes(path)
119
77
  path = '/' + path unless path.start_with?('/')
@@ -121,13 +79,24 @@ module Ichiban
121
79
  path
122
80
  end
123
81
 
82
+ def partial(path)
83
+ file = Ichiban::PartialHTMLFile.new(
84
+ File.join('html', path)
85
+ )
86
+ # Record the dependency like this: 'folder/partial-name' => 'folder/included-file.html'
87
+ Ichiban::Dependencies.update('.partial_dependencies.json', file.partial_name, @_template_path)
88
+ compiler = Ichiban::HTMLCompiler.new(file)
89
+ compiler.ivars = to_hash # to_hash is inherited from Erubis::Context. It's a hash of the instance variables.
90
+ compiler.compile_to_str
91
+ end
92
+
124
93
  def relative_url_root
125
94
  Ichiban.config.relative_url_root
126
95
  end
127
96
 
128
97
  def stylesheet_link_tag(css_file, media = 'screen')
129
98
  css_file = css_file + '.css' unless css_file.end_with?('.css')
130
- href = normalize_path(::File.join('/stylesheets', css_file))
99
+ href = normalize_path(File.join('/css', css_file))
131
100
  tag 'link', 'href' => href, 'type' => 'text/css', 'rel' => 'stylesheet', 'media' => media
132
101
  end
133
102