ichiban 0.0.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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