ichiban 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/lib/ichiban/command.rb +13 -1
  2. data/lib/ichiban/config.rb +7 -17
  3. data/lib/ichiban/deleter.rb +24 -0
  4. data/lib/ichiban/dependencies.rb +33 -0
  5. data/lib/ichiban/file.rb +115 -0
  6. data/lib/ichiban/helpers.rb +6 -10
  7. data/lib/ichiban/html_compiler.rb +61 -0
  8. data/lib/ichiban/logger.rb +59 -3
  9. data/lib/ichiban/markdown.rb +34 -0
  10. data/lib/ichiban/watcher.rb +35 -3
  11. data/lib/ichiban.rb +44 -17
  12. metadata +123 -74
  13. data/.gitignore +0 -5
  14. data/Gemfile +0 -4
  15. data/README +0 -56
  16. data/bin/ichiban.rb +0 -6
  17. data/ichiban.gemspec +0 -25
  18. data/klass.rb +0 -7
  19. data/lib/ichiban/compilation.rb +0 -93
  20. data/lib/ichiban/erb_page.rb +0 -16
  21. data/lib/ichiban/files.rb +0 -105
  22. data/lib/ichiban/layouts.rb +0 -10
  23. data/lib/ichiban/loading.rb +0 -19
  24. data/lib/ichiban/mapping.rb +0 -38
  25. data/lib/ichiban/path.rb +0 -55
  26. data/lib/ichiban/script_runner.rb +0 -33
  27. data/lib/ichiban/tasks.rb +0 -23
  28. data/lib/ichiban/version.rb +0 -3
  29. data/sample/Rakefile +0 -2
  30. data/sample/compiled/about.html +0 -13
  31. data/sample/compiled/bad.html +0 -0
  32. data/sample/compiled/images/check.png +0 -0
  33. data/sample/compiled/index.html +0 -13
  34. data/sample/compiled/javascripts/interaction.js +0 -1
  35. data/sample/compiled/staff/_employee.html +0 -13
  36. data/sample/compiled/staff/andre-marques.html +0 -13
  37. data/sample/compiled/staff/index.html +0 -17
  38. data/sample/compiled/staff/jarrett-colby.html +0 -13
  39. data/sample/compiled/stylesheets/reset.css +0 -1
  40. data/sample/compiled/stylesheets/screen.css +0 -1
  41. data/sample/config.rb +0 -3
  42. data/sample/content/about.html +0 -1
  43. data/sample/content/bad.html +0 -3
  44. data/sample/content/index.html +0 -1
  45. data/sample/content/staff/_employee.html +0 -1
  46. data/sample/content/staff/index.html +0 -6
  47. data/sample/data/employees.csv +0 -2
  48. data/sample/errors/404.html +0 -1
  49. data/sample/helpers/staff_helper.rb +0 -5
  50. data/sample/images/check.png +0 -0
  51. data/sample/javascripts/interaction.js +0 -1
  52. data/sample/layouts/default.html +0 -13
  53. data/sample/models/employee.rb +0 -16
  54. data/sample/scripts/bad.rb +0 -1
  55. data/sample/scripts/staff.rb +0 -8
  56. data/sample/stylesheets/reset.css +0 -1
  57. data/sample/stylesheets/screen.scss +0 -5
  58. data/spec/integration_spec.rb +0 -89
  59. data/spec/path_spec.rb +0 -15
  60. data/spec/spec_helper.rb +0 -19
@@ -1,11 +1,23 @@
1
1
  module Ichiban
2
2
  class Command
3
3
  def initialize(args)
4
+ @task = args.shift
4
5
  @args = args
5
6
  end
6
7
 
8
+ def print_usage
9
+ puts "Usage: ichiban <command>"
10
+ puts " Available commands: watch"
11
+ end
12
+
7
13
  def run
8
- raise 'TODO'
14
+ Ichiban.project_root = Dir.getwd
15
+ case @task
16
+ when 'watch'
17
+ Ichiban::Watcher.new.start
18
+ else
19
+ print_usage
20
+ end
9
21
  end
10
22
  end
11
23
  end
@@ -5,27 +5,17 @@ module Ichiban
5
5
  @config
6
6
  end
7
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
8
  class Config
23
- attr_accessor :project_root
24
-
25
9
  attr_writer :relative_url_root
26
10
 
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)
14
+ load config_file
15
+ end
16
+
27
17
  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 = \'/\'')
18
+ @relative_url_root || raise("Ichiban.config.relative_url_root not set. Set inside block in config.rb like this: cfg.relative_url_root = '/'")
29
19
  end
30
20
  end
31
21
  end
@@ -0,0 +1,24 @@
1
+ module Ichiban
2
+ class Deleter
3
+ # Deletes a file's associated destination file, if any.
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.
8
+ if file and file.has_dest?
9
+ dest = file.dest
10
+ else
11
+ dest = nil
12
+ end
13
+ if dest and ::File.exists?(dest)
14
+ puts 'yep'
15
+ FileUtils.rm(dest)
16
+ else
17
+ puts 'nope'
18
+ end
19
+
20
+ # Log the deletion(s)
21
+ Ichiban.logger.deletion(path, dest)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ module Ichiban
2
+ module Dependencies
3
+ @graphs = {}
4
+
5
+ # graph_file_path is an absolute path.
6
+ def self.graph(graph_file_path)
7
+ ensure_graph_initialized(graph_file_path)
8
+ @graphs[graph_file_path]
9
+ end
10
+
11
+ def self.ensure_graph_initialized(graph_file_path)
12
+ unless @graphs[graph_file_path]
13
+ if ::File.exists?(graph_file_path)
14
+ @graphs[graph_file_path] = JSON.parse(::File.read(graph_file_path))
15
+ else
16
+ @graphs[graph_file_path] = {}
17
+ end
18
+ end
19
+ end
20
+
21
+ # 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.
23
+ def self.update(graph_file_path, ind, dep)
24
+ ensure_graph_initialized(graph_file_path)
25
+ graph = @graphs[graph_file_path]
26
+ graph[ind] ||= []
27
+ graph[ind] << dep unless graph[ind].include?(dep)
28
+ ::File.open(graph_file_path, 'w') do |f|
29
+ f << JSON.generate(graph)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,115 @@
1
+ module Ichiban
2
+ class File
3
+ attr_reader :abs
4
+
5
+ # Returns an absolute path in the compiled directory
6
+ def dest
7
+ ::File.join(Ichiban.project_root, 'compiled', dest_rel_to_compiled)
8
+ end
9
+
10
+ # Returns a new instance based on an absolute path. Will automatically pick the right subclass.
11
+ # Return nil if the file is not recognized.
12
+ def self.from_abs(abs)
13
+ rel = abs.slice(Ichiban.project_root.length..-1) # Relative to project root
14
+ rel.sub!(/^\//, '') # Remove leading slash
15
+ if rel.start_with?('html') and rel.end_with?('.html')
16
+ Ichiban::HTMLFile.new(rel)
17
+ elsif rel.start_with?('layouts') and rel.end_with?('.html')
18
+ Ichiban::LayoutFIle.new(rel)
19
+ elsif rel.start_with?('assets/js')
20
+ Ichiban::JSFile.new(rel)
21
+ elsif rel.start_with?('assets/css') and rel.end_with?('.css')
22
+ Ichiban::CSSFile.new(rel)
23
+ elsif rel.start_with?('assets/css') and rel.end_with?('.scss')
24
+ Ichiban::SCSSFile.new(rel)
25
+ elsif rel.start_with?('assets/img')
26
+ Ichiban::ImageFile.new(rel)
27
+ elsif rel.start_with?('assets/misc')
28
+ Ichiban::MiscAssetFile.new(rel)
29
+ elsif rel.start_with?('models')
30
+ Ichiban::ModelFile.new(rel)
31
+ elsif rel.start_with?('data')
32
+ Ichiban::DataFile.new(rel)
33
+ elsif rel.start_with?('scripts')
34
+ Ichiban::ScriptFile.new(rel)
35
+ elsif rel.start_with?('helpers')
36
+ Ichiban::HelperFile.new(rel)
37
+ else
38
+ nil
39
+ end
40
+ end
41
+
42
+ def has_dest?
43
+ respond_to?(:dest_rel_to_compiled)
44
+ end
45
+
46
+ def initialize(rel)
47
+ @rel = rel
48
+ @abs = ::File.join(Ichiban.project_root, rel)
49
+ end
50
+
51
+ # Returns a new path where the old extension is replaced with new_ext
52
+ def replace_ext(path, new_ext)
53
+ path.sub(/\..+$/, '.' + new_ext)
54
+ end
55
+ end
56
+
57
+ class HTMLFile < File
58
+ def dest_rel_to_compiled
59
+ d = @rel.slice('html/'.length..-1)
60
+ (d.end_with?('.markdown') or d.end_with?('.md')) ? replace_ext(d, 'html') : d
61
+ end
62
+
63
+ def update
64
+ Ichiban::HTMLCompiler.new(self).compile
65
+ end
66
+ end
67
+
68
+ class LayoutFile < File
69
+ end
70
+
71
+ class JSFile < File
72
+ def dest_rel_to_compiled
73
+ File.join('js', @rel.slice('assets/js/'.length..-1))
74
+ end
75
+ end
76
+
77
+ class CSSFile < File
78
+ def dest_rel_to_compiled
79
+ File.join('css', @rel.slice('assets/css/'.length..-1))
80
+ end
81
+ end
82
+
83
+ class SCSSFile < File
84
+ def dest_rel_to_compiled
85
+ replace_ext(
86
+ File.join('css', @rel.slice('assets/css/'.length..-1)),
87
+ 'css'
88
+ )
89
+ end
90
+ end
91
+
92
+ class ImageFile < File
93
+ def dest_rel_to_compiled
94
+ File.join('img', @rel.slice('assets/img/'.length..-1))
95
+ end
96
+ end
97
+
98
+ class MiscAssetFile < File
99
+ def dest_rel_to_compiled
100
+ @rel.slice('assets/misc/'.length..-1)
101
+ end
102
+ end
103
+
104
+ class ModelFile < File
105
+ end
106
+
107
+ class DataFile < File
108
+ end
109
+
110
+ class ScriptFile < File
111
+ end
112
+
113
+ class HelperFile < File
114
+ end
115
+ end
@@ -13,10 +13,6 @@ module Ichiban
13
13
  @_erb_out << str
14
14
  end
15
15
 
16
- def content_for(ivar_name, &block)
17
- instance_variable_set '@' + ivar_name, capture(&block)
18
- end
19
-
20
16
  def content_tag(*args)
21
17
  options = args.extract_options!
22
18
  name = args.shift
@@ -32,7 +28,7 @@ module Ichiban
32
28
 
33
29
  def javascript_include_tag(js_file)
34
30
  js_file = js_file + '.js' unless js_file.end_with?('.js')
35
- path = normalize_path(File.join('/javascripts', js_file))
31
+ path = normalize_path(::File.join('/javascripts', js_file))
36
32
  content_tag 'script', 'type' => 'text/javascript', 'src' => path
37
33
  end
38
34
 
@@ -42,7 +38,7 @@ module Ichiban
42
38
 
43
39
  alias_method :layouts, :layout
44
40
 
45
- def limit_options(hash, keys = [])
41
+ def _limit_options(hash, keys = [])
46
42
  keys = keys.collect(&:to_s)
47
43
  hash.inject({}) do |result, (key, value)|
48
44
  result[key] = value if (keys.include?(key.to_s) or (block_given? and yield(key, value)))
@@ -86,11 +82,11 @@ module Ichiban
86
82
  # If you don't specify a section, and your URLs don't have leading slashes,
87
83
  # the hrefs will use relative URLs.
88
84
  def nav(items, options = {})
89
- ul_options = limit_options(options, %w(id class)) { |key, value| key.to_s.start_with?('data-') }
85
+ ul_options = _limit_options(options, %w(id class)) { |key, value| key.to_s.start_with?('data-') }
90
86
  content_tag('ul', ul_options) do
91
87
  items.inject('') do |lis, (text, path, attrs)|
92
88
  if options[:section]
93
- path = File.join(options[:section], path)
89
+ path = ::File.join(options[:section], path)
94
90
  end
95
91
  path = normalize_path(path)
96
92
  lis + content_tag('li', (attrs or {})) do
@@ -108,7 +104,7 @@ module Ichiban
108
104
  # Otherwise, it will remain relative.
109
105
  def normalize_path(path)
110
106
  if path.start_with?('/')
111
- File.join(relative_url_root, path)
107
+ ::File.join(relative_url_root, path)
112
108
  else
113
109
  path
114
110
  end
@@ -131,7 +127,7 @@ module Ichiban
131
127
 
132
128
  def stylesheet_link_tag(css_file, media = 'screen')
133
129
  css_file = css_file + '.css' unless css_file.end_with?('.css')
134
- href = normalize_path(File.join('/stylesheets', css_file))
130
+ href = normalize_path(::File.join('/stylesheets', css_file))
135
131
  tag 'link', 'href' => href, 'type' => 'text/css', 'rel' => 'stylesheet', 'media' => media
136
132
  end
137
133
 
@@ -0,0 +1,61 @@
1
+ module Ichiban
2
+ class HTMLCompiler
3
+ def compile
4
+ ::File.open(@html_file.dest, 'w') do |f|
5
+ f << compile_to_str
6
+ end
7
+ Ichiban.logger.compilation(@html_file.abs, @html_file.dest)
8
+ end
9
+
10
+ def compile_to_str
11
+ # Compile the HTML of the content page, but not the layouts (yet)
12
+ ctx = Ichiban::HTMLCompiler::Context.new(:_current_path => @html_file.dest_rel_to_compiled)
13
+ inner_html = Eruby.new(::File.read(@html_file.abs)).evaluate(ctx)
14
+
15
+ # Compile Markdown if necessary
16
+ if (@html_file.abs.end_with?('.markdown') or @html_file.abs.end_with?('.md'))
17
+ inner_html = Ichiban::Markdown.compile(inner_html) # Will look for installed Markdown gems
18
+ end
19
+
20
+ # Layouts
21
+ wrap_in_layouts(ctx, inner_html)
22
+ end
23
+
24
+ # Takes an instance of Ichiban::HTMLFile
25
+ def initialize(html_file)
26
+ @html_file = html_file
27
+ end
28
+
29
+ def wrap_in_layouts(ctx, inner_rhtml)
30
+ ctx.layout_stack.reverse.inject(inner_rhtml) do |html, layout_name|
31
+ layout_path = ::File.join(Ichiban.project_root, 'layouts', layout_name + '.html')
32
+ unless ::File.exists?(layout_path)
33
+ raise "Layout does not exist: #{layout_path}"
34
+ end
35
+ eruby = Eruby.new(
36
+ ::File.read(layout_path),
37
+ :filename => layout_path
38
+ )
39
+ html = eruby.evaluate(ctx) { html }
40
+ Ichiban::Dependencies.update('.layout_dependencies.json', layout_name, @html_file.abs)
41
+ html
42
+ end
43
+ end
44
+
45
+ class Eruby < Erubis::Eruby
46
+ def add_preamble(src)
47
+ src << "@_erb_out = _buf = '';"
48
+ end
49
+ end
50
+
51
+ class Context < Erubis::Context
52
+ include Ichiban::Helpers
53
+ include Erubis::XmlHelper
54
+ include ERB::Util # Give us #h
55
+
56
+ def layout_stack
57
+ @_layout_stack or ['default']
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,16 +4,72 @@ module Ichiban
4
4
  end
5
5
 
6
6
  class Logger
7
+ def self.ansi?
8
+ @ansi
9
+ end
10
+
11
+ def ansi?
12
+ self.class.ansi?
13
+ end
14
+
7
15
  def compilation(src, dst)
8
- out "#{src} => #{dst}"
16
+ src = src.slice(Ichiban.project_root.length + 1..-1)
17
+ dst = dst.slice(Ichiban.project_root.length + 1..-1)
18
+ msg = "#{src} -> #{dst}"
19
+ if ansi?
20
+ msg = ANSI.color(msg, :green)
21
+ end
22
+ out msg
23
+ end
24
+
25
+ def deletion(src, dst = nil)
26
+ src = src.slice(Ichiban.project_root.length + 1..-1)
27
+ if dst
28
+ dst = dst.slice(Ichiban.project_root.length + 1..-1)
29
+ end
30
+ if dst
31
+ msg = "Deleted: #{src} -> #{dst}"
32
+ else
33
+ msg = "Deleted: #{src}"
34
+ end
35
+ if ansi?
36
+ msg = ANSI.color(msg, :cyan)
37
+ end
38
+ out msg
9
39
  end
10
40
 
11
41
  def exception(exc)
12
- out "#{exc.class.to_s}: #{exc.message}\n" + exc.backtrace.collect { |line| ' ' + line }.join("\n")
42
+ msg = "#{exc.class.to_s}: #{exc.message}\n" + exc.backtrace.collect { |line| ' ' + line }.join("\n")
43
+ if ansi?
44
+ msg = ANSI.color(msg, :red)
45
+ end
46
+ out msg
47
+ end
48
+
49
+ def initialize
50
+ @out = STDOUT
51
+ end
52
+
53
+ def out=(io)
54
+ @out = io
13
55
  end
14
56
 
15
57
  def out(msg)
16
- puts msg
58
+ @out.puts msg
59
+ end
60
+
61
+ def warn(msg)
62
+ if ansi?
63
+ msg = ANSI.color(msg, :red)
64
+ end
65
+ out msg
66
+ end
67
+
68
+ begin
69
+ require 'ansi'
70
+ @ansi = true
71
+ rescue LoadError
72
+ Ichiban.logger.out("Try `gem install ansi` for colorized output")
17
73
  end
18
74
  end
19
75
  end
@@ -0,0 +1,34 @@
1
+ module Ichiban
2
+ module Markdown
3
+ def self.compile(src)
4
+ require_markdown
5
+ case @strategy
6
+ when :redcarpet
7
+ @redcarpet.render(src)
8
+ when :maruku
9
+ Maruku.new(src).to_html
10
+ when :rdiscount
11
+ RDiscount.new(src).to_html
12
+ else
13
+ raise "unrecognized @strategy: #{@strategy}"
14
+ end
15
+ end
16
+
17
+ def self.require_markdown
18
+ unless @markdown_loaded
19
+ case Ichiban.try_require('redcarpet', 'maruku', 'rdiscount')
20
+ when 'redcarpet'
21
+ @redcarpet = Redcarpet::Markdown.new(Redcarpet::Render::XHTML.new)
22
+ @strategy = :redcarpet
23
+ when 'maruku'
24
+ @strategy = :maruku
25
+ when 'rdiscount'
26
+ @strategy = :rdiscount
27
+ else
28
+ raise "Your Ichiban project contains at least one Markdown file. To process it, you need to install either the redcarpet or maruku gem."
29
+ end
30
+ @markdown_loaded = true
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,39 @@
1
1
  module Ichiban
2
- class Watcher
3
- def watch
4
- raise 'TODO'
2
+ class Watcher
3
+ def initialize(options = {})
4
+ @options = {
5
+ :latency => 0.5
6
+ }.merge(options)
7
+ end
8
+
9
+ def start
10
+ @listener = Listen.to(
11
+ ::File.join(Ichiban.project_root, 'html')#,
12
+ #::File.join(Ichiban.project_root, 'assets')
13
+ )
14
+ .ignore(/.listen_test$/)
15
+ .latency(@options[:latency])
16
+ .change do |modified, added, deleted|
17
+ begin
18
+ (modified + added).each do |path|
19
+ if file = Ichiban::File.from_abs(path)
20
+ file.update
21
+ end
22
+ end
23
+ rescue => exc
24
+ Ichiban.logger.exception(exc)
25
+ end
26
+ deleted.each do |path|
27
+ Ichiban::Deleter.new.delete(path)
28
+ end
29
+ end.start(false) # nonblocking
30
+ end
31
+
32
+ def stop
33
+ if @listener
34
+ @listener.stop
35
+ @listener = nil
36
+ end
5
37
  end
6
38
  end
7
39
  end
data/lib/ichiban.rb CHANGED
@@ -1,25 +1,52 @@
1
- require 'csv'
1
+ # Standard lib
2
2
  require 'fileutils'
3
+ require 'json'
4
+ require 'erb' # Just for the helpers
5
+
6
+ # Gems
3
7
  require 'active_support/core_ext/class/attribute'
4
8
  require 'active_support/core_ext/object/blank'
5
9
  require 'active_support/inflector'
6
- require 'erubis'
7
- require 'maruku'
8
10
  require 'sass'
11
+ require 'listen'
12
+ require 'erubis'
13
+ require 'rake'
9
14
 
10
- # Order matters!
11
- require 'ichiban/command'
12
- require 'ichiban/layouts'
13
- require 'ichiban/helpers'
14
- require 'ichiban/compilation'
15
+ # Ichiban files. Order matters!
15
16
  require 'ichiban/config'
16
- require 'ichiban/erb_page'
17
- require 'ichiban/files'
18
17
  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'
18
+ require 'ichiban/command'
19
+ require 'ichiban/watcher'
20
+ require 'ichiban/deleter'
21
+ require 'ichiban/file'
22
+ require 'ichiban/helpers'
23
+ require 'ichiban/html_compiler'
24
+ require 'ichiban/markdown'
25
+ require 'ichiban/dependencies'
26
+ require 'ichiban/helpers'
27
+
28
+ module Ichiban
29
+ # In addition to setting the variable, this loads the config file
30
+ def self.project_root=(path)
31
+ @project_root = path
32
+ if path # It's valid to set project_root to nil, though this would likely only happen in tests
33
+ Ichiban::Config.load_file
34
+ end
35
+ end
36
+
37
+ def self.project_root
38
+ @project_root
39
+ end
40
+
41
+ # Try to load the libraries
42
+ def self.try_require(*gems)
43
+ gems.each do |gem|
44
+ begin
45
+ require gem
46
+ return gem
47
+ rescue LoadError
48
+ end
49
+ end
50
+ false
51
+ end
52
+ end