laze 0.2.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 (65) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +150 -0
  5. data/Rakefile +76 -0
  6. data/VERSION.yml +5 -0
  7. data/bin/laze +126 -0
  8. data/examples/website/includes/tagline.html +1 -0
  9. data/examples/website/input/contact/email.md +6 -0
  10. data/examples/website/input/css/screen.css +5 -0
  11. data/examples/website/input/css/test.less +6 -0
  12. data/examples/website/input/img/ruby.gif +0 -0
  13. data/examples/website/input/img/test.jpg +0 -0
  14. data/examples/website/input/img/test.png +0 -0
  15. data/examples/website/input/index.md +13 -0
  16. data/examples/website/input/js/foo.js +1 -0
  17. data/examples/website/input/js/lib.js +5 -0
  18. data/examples/website/layouts/default.html +15 -0
  19. data/examples/website/layouts/subpage.html +5 -0
  20. data/examples/website/laze.yml +1 -0
  21. data/features/create_sites.feature +73 -0
  22. data/features/plugins.feature +49 -0
  23. data/features/site_data.feature +26 -0
  24. data/features/step_definitions/laze_steps.rb +77 -0
  25. data/features/support/env.rb +17 -0
  26. data/lib/laze.rb +64 -0
  27. data/lib/laze/asset.rb +7 -0
  28. data/lib/laze/core_extensions.rb +15 -0
  29. data/lib/laze/item.rb +59 -0
  30. data/lib/laze/javascript.rb +5 -0
  31. data/lib/laze/layout.rb +105 -0
  32. data/lib/laze/page.rb +33 -0
  33. data/lib/laze/plugins.rb +56 -0
  34. data/lib/laze/plugins/cache_buster.rb +49 -0
  35. data/lib/laze/plugins/css_imports.rb +42 -0
  36. data/lib/laze/plugins/cssmin.rb +31 -0
  37. data/lib/laze/plugins/image_check.rb +28 -0
  38. data/lib/laze/plugins/image_optimizer.rb +52 -0
  39. data/lib/laze/plugins/js_requires.rb +63 -0
  40. data/lib/laze/plugins/jsmin.rb +31 -0
  41. data/lib/laze/plugins/less.rb +35 -0
  42. data/lib/laze/plugins/robots.rb +28 -0
  43. data/lib/laze/plugins/sitemap.rb +63 -0
  44. data/lib/laze/renderer.rb +47 -0
  45. data/lib/laze/renderers/javascript_renderer.rb +16 -0
  46. data/lib/laze/renderers/page_renderer.rb +40 -0
  47. data/lib/laze/renderers/stylesheet_renderer.rb +16 -0
  48. data/lib/laze/secretary.rb +74 -0
  49. data/lib/laze/section.rb +39 -0
  50. data/lib/laze/store.rb +51 -0
  51. data/lib/laze/stores/filesystem.rb +127 -0
  52. data/lib/laze/stylesheet.rb +6 -0
  53. data/lib/laze/target.rb +56 -0
  54. data/lib/laze/targets/filesystem.rb +41 -0
  55. data/test/helper.rb +12 -0
  56. data/test/test_assets.rb +28 -0
  57. data/test/test_core_extensions.rb +19 -0
  58. data/test/test_item.rb +59 -0
  59. data/test/test_layout.rb +47 -0
  60. data/test/test_renderer.rb +71 -0
  61. data/test/test_renderers.rb +40 -0
  62. data/test/test_secretary.rb +48 -0
  63. data/test/test_store.rb +18 -0
  64. data/test/test_target.rb +84 -0
  65. metadata +207 -0
@@ -0,0 +1,127 @@
1
+ module Laze
2
+ module Stores #:nodoc:
3
+ class Filesystem < Store
4
+ # Path from the project root to where includes are stored.
5
+ INCLUDES_DIR = 'includes'
6
+
7
+ # Path from the project root to where layouts are stored.
8
+ LAYOUTS_DIR = 'layouts'
9
+
10
+ # Path from the project root to where all input files are stored.
11
+ INPUT_DIR = 'input'
12
+
13
+ # Create a new filesystem object for a given project root directory,
14
+ # which defaults to the current working directory.
15
+ def initialize(root = Dir.pwd)
16
+ @root = root
17
+ super()
18
+ end
19
+
20
+ def each(&block)
21
+ scan_directory(File.join(@root, INPUT_DIR), &block)
22
+ end
23
+
24
+ def find_layout(layout_name)
25
+ FileWithMetadata.new(read_template_file(layout_name, LAYOUTS_DIR)).to_layout
26
+ end
27
+
28
+ def read_template_file(include_name, from_dir = INCLUDES_DIR)
29
+ Laze.debug "Reading template file #{include_name}"
30
+ raise FileSystemException, "Illegal filename '#{include_name}'" unless include_name =~ /^[^.\/][a-zA-Z0-9_\/]+$/
31
+ full_path = File.join(@root, from_dir, "#{include_name}.html")
32
+ raise FileSystemException, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(@root)}/
33
+ File.read(full_path)
34
+ end
35
+
36
+ private
37
+
38
+ def scan_directory(path)
39
+ Laze.debug "Recursing into #{path}"
40
+
41
+ Dir.foreach(path) do |filename|
42
+ # Skip directories
43
+ next if %w[. ..].include?(filename)
44
+
45
+ # Get current entry path
46
+ full_path = File.join(path, filename)
47
+
48
+ if File.file?(full_path)
49
+ relative_path = File.dirname(full_path.sub(File.join(@root, 'input/'), ''))
50
+ file_content = File.read(full_path)
51
+ file = FileWithMetadata.new(file_content, { :filename => filename, :path => relative_path })
52
+ yield case File.extname(filename)
53
+ when /\.(css|less)/: file.to_stylesheet
54
+ when '.js': file.to_javascript
55
+ else
56
+ if file_content =~ /---|\{%.+%\}|\{\{.+\}\}/
57
+ file.to_page
58
+ else
59
+ # just copy over without modification
60
+ file.to_item
61
+ end
62
+ end
63
+
64
+ elsif File.directory?(full_path)
65
+ section = Section.new({ :filename => filename })
66
+ scan_directory(full_path) do |subitem|
67
+ section << subitem
68
+ end
69
+ yield section
70
+
71
+ else
72
+ Laze.debug "Skipping #{path}"
73
+ end
74
+ end
75
+ end
76
+
77
+ class FileWithMetadata #:nodoc:
78
+ attr_reader :content, :properties
79
+
80
+ METADATA_SEPARATOR = /^---$\s*/
81
+
82
+ def initialize(file_contents, extra_metadata = {})
83
+ @file_contents, @properties = file_contents, extra_metadata
84
+ split
85
+ end
86
+
87
+ def has?(key)
88
+ properties.has_key?(key)
89
+ end
90
+
91
+ def to_item
92
+ Item.new(properties, content)
93
+ end
94
+
95
+ def to_page
96
+ Page.new(properties, content)
97
+ end
98
+
99
+ def to_layout
100
+ Layout.new(properties, content)
101
+ end
102
+
103
+ def to_stylesheet
104
+ Stylesheet.new(properties, content)
105
+ end
106
+
107
+ def to_javascript
108
+ Javascript.new(properties, content)
109
+ end
110
+
111
+ private
112
+
113
+ def split
114
+ @content = @file_contents
115
+ if @file_contents =~ METADATA_SEPARATOR
116
+ yaml_string, @content = @file_contents.split(METADATA_SEPARATOR, 2)
117
+ @properties = yaml_string_to_properties(yaml_string).merge(@properties)
118
+ end
119
+ end
120
+
121
+ def yaml_string_to_properties(yaml_string)
122
+ properties = YAML.load(yaml_string).symbolize_keys
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,6 @@
1
+ module Laze
2
+ # A special Item aimed at Cascading Stylesheets (CSS-files)
3
+ class Stylesheet < Asset
4
+ include_plugins :stylesheet
5
+ end
6
+ end
@@ -0,0 +1,56 @@
1
+ module Laze
2
+ class Target
3
+
4
+ # Generic exception to be raised for store-specific exceptions.
5
+ TargetException = Class.new(Exception)
6
+
7
+ # Exception for when interacting with the filesystem goes bad.
8
+ FileSystemException = Class.new(TargetException)
9
+
10
+ @targets = []
11
+
12
+ # The base directory to create all the files in. This is relative
13
+ # the location where laze is run from.
14
+ attr_reader :output_dir
15
+
16
+ # Find a target deployment engine by name and return its class.
17
+ #
18
+ # When loading <tt>Laze::Targets::Filesystem</tt> you would call:
19
+ #
20
+ # Target.find(:filesystem)
21
+ #
22
+ def self.find(kind)
23
+ targets = @targets.select { |s| s.name.to_s.split('::').last.downcase.to_sym == kind }
24
+ raise TargetException, 'No such target.' unless targets.any?
25
+ targets.first
26
+ end
27
+
28
+ def self.inherited(child) #:nodoc:
29
+ @targets << child
30
+ end
31
+
32
+ def initialize(output_dir) #:nodoc:
33
+ @output_dir = output_dir
34
+ reset
35
+ Laze::Plugins.each(:target) { |plugin| extend plugin }
36
+ end
37
+
38
+ # Finalize the generation and process any after hooks.
39
+ def save
40
+ raise 'This is a generic target. Please use a subclass.'
41
+ end
42
+
43
+ # Given an item this will create the output file in the right output
44
+ # location.
45
+ def create(item)
46
+ raise 'This is a generic target. Please use a subclass.'
47
+ end
48
+
49
+ # Empty the current output directory
50
+ def reset
51
+ FileUtils.rm_rf(output_dir) if File.directory?(output_dir)
52
+ FileUtils.mkdir(output_dir) unless File.directory?(output_dir)
53
+ Laze.debug "Emptied output directory #{output_dir}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,41 @@
1
+ module Laze
2
+ module Targets #:nodoc:
3
+ class Filesystem < Target
4
+
5
+ # Manifest an item -- write it to disk
6
+ def create(item)
7
+ case item
8
+ when Page, Asset: create_page(item)
9
+ when Section: create_section(item)
10
+ when Item: copy_file(item)
11
+ end
12
+ end
13
+
14
+ def save
15
+ # already done...
16
+ end
17
+
18
+ private
19
+
20
+ def copy_file(item)
21
+ File.open(File.join(output_dir, item.properties[:path], item.properties[:filename]), 'w') { |f| f.write item.content }
22
+ end
23
+
24
+ def create_page(item)
25
+ File.open(dir(item), 'w') { |f| f.write Renderer.render(item) }
26
+ end
27
+
28
+ def create_section(item)
29
+ FileUtils.mkdir(dir(item))
30
+ item.each { |subitem| create(subitem) }
31
+ end
32
+
33
+ # Get the correct path for a given item.
34
+ # This finds all the ancestors for an item, joins them together
35
+ # as directories and returns the path name.
36
+ def dir(item)
37
+ File.join(output_dir, *(item.ancestors.map{ |i| i.filename } << item.filename))
38
+ end
39
+ end
40
+ end
41
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ require 'laze'
9
+
10
+ class Test::Unit::TestCase
11
+ include Laze
12
+ end
@@ -0,0 +1,28 @@
1
+ require 'helper'
2
+
3
+ class TestAssets < Test::Unit::TestCase
4
+ context "generic asset" do
5
+ should "take content and options" do
6
+ asset = Asset.new({ :title => 'foo' }, 'bar')
7
+ assert_equal({:title => 'foo'}, asset.properties)
8
+ assert_equal('bar', asset.content)
9
+ end
10
+
11
+ should "return its filename" do
12
+ asset = Asset.new({:filename => 'foo'}, 'bar')
13
+ assert_equal('foo', asset.filename)
14
+ end
15
+ end
16
+
17
+ context "stylesheet" do
18
+ setup do
19
+ @less_stylesheet = Stylesheet.new({ :filename => 'base.less' }, 'foo')
20
+ @stylesheet = Stylesheet.new({ :filename => 'base.css' }, 'foo')
21
+ end
22
+
23
+ should "convert the filename to css" do
24
+ assert_equal('base.css', @less_stylesheet.filename)
25
+ assert_equal('base.css', @stylesheet.filename)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ require 'helper'
2
+
3
+ class TestCoreExtensions < Test::Unit::TestCase
4
+ context "hashes" do
5
+ setup do
6
+ @a = { 'foo' => 'bar' }
7
+ @b = { :foo => 'bar' }
8
+ end
9
+
10
+ should "convert keys to strings" do
11
+ assert_equal(@a, @b.stringify_keys)
12
+ end
13
+
14
+ should "convert keys to symbols" do
15
+ assert_equal(@b, @a.symbolize_keys)
16
+ end
17
+ end
18
+
19
+ end
data/test/test_item.rb ADDED
@@ -0,0 +1,59 @@
1
+ require 'helper'
2
+
3
+ class TestItemPageAndSection < Test::Unit::TestCase
4
+ context "with children" do
5
+
6
+ setup do
7
+ @x = Page.new({ :title => 'x', :filename => 'foo' }, 'page x')
8
+ @y = Page.new({ :title => 'y' }, 'page y')
9
+ @z = Page.new({ :title => 'z' }, 'page z')
10
+ @a = Section.new({ :title => 'a' })
11
+ @b = Section.new({ :title => 'b' })
12
+ @a.add_item @x
13
+ @b.add_item @y
14
+ @b.add_item @z
15
+ @a.add_item @b
16
+ end
17
+
18
+ should "tell it has a property" do
19
+ assert @x.has?(:title)
20
+ assert !@x.has?(:foo)
21
+ end
22
+
23
+ should "inspect nicely" do
24
+ assert_match(/#<Laze::Page:0x\w+? foo>/, @x.inspect)
25
+ end
26
+
27
+ should "tell its filename" do
28
+ assert_equal('foo', @x.filename)
29
+ assert_equal(@x.filename, @x.properties[:filename])
30
+ end
31
+
32
+ should "convert to string by its filename" do
33
+ assert_equal('foo', @x.to_s)
34
+ end
35
+
36
+ should 'have subitems' do
37
+ assert_equal(4, @a.number_of_subitems)
38
+ end
39
+
40
+ should "remove subitems" do
41
+ assert_equal(3, @a.remove_item(@x).number_of_subitems)
42
+ assert_nil(@x.parent)
43
+ end
44
+
45
+ should "enumerate over subitems" do
46
+ i = 0
47
+ @b.each do |item|
48
+ assert_kind_of(Item, item)
49
+ i += 1
50
+ end
51
+ assert_equal(2, i)
52
+ end
53
+
54
+ should "count the number of ancestors" do
55
+ assert_equal(2, @y.ancestors.size)
56
+ assert_equal(0, @a.ancestors.size)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,47 @@
1
+ require 'helper'
2
+
3
+ class TestLayout < Test::Unit::TestCase
4
+ context "when creating" do
5
+ should "store content" do
6
+ assert_equal('foo', Layout.new({}, 'foo').content)
7
+ end
8
+
9
+ should "return nil for a nil layout" do
10
+ assert_nil(Layout.find(nil))
11
+ end
12
+
13
+ should "ask secretary for a layout" do
14
+ store = mock()
15
+ current = mock()
16
+ store.expects(:find_layout).with('foo').returns('bar')
17
+ current.expects(:store).returns(store)
18
+ Secretary.expects(:current).returns(current)
19
+ assert_equal('bar', Layout.find('foo'))
20
+ end
21
+ end
22
+
23
+ context "simple layout" do
24
+ setup do
25
+ @layout = Layout.new({ :layout => 'foo' }, 'bar: {{ yield }}')
26
+ end
27
+
28
+ should "return its layout" do
29
+ assert_equal('foo', @layout.layout)
30
+ end
31
+
32
+ should "wrap itself around a string" do
33
+ assert_equal("bar: baz", @layout.wrap('baz'))
34
+ end
35
+ end
36
+
37
+ context "complex layout" do
38
+ setup do
39
+ @layout = Layout.new({ :layout => 'foo' }, "bar\n {{ yield }}\n")
40
+ end
41
+
42
+ should "preserve whitespace indent" do
43
+ assert_equal("bar\n foo", @layout.wrap('foo'))
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,71 @@
1
+ require 'helper'
2
+
3
+ class TestRenderer < Test::Unit::TestCase
4
+ context 'with bad options' do
5
+ should 'raise an error with no arguments' do
6
+ assert_raise(ArgumentError) { Renderer.new }
7
+ end
8
+
9
+ should 'raise an error with too many arguments' do
10
+ assert_raise(ArgumentError) { Renderer.new(0, 1, 2) }
11
+ end
12
+
13
+ should "raise an error when not passed in an item or string" do
14
+ assert_raise(ArgumentError) { Renderer.new(0) }
15
+ assert_nothing_raised(ArgumentError) { Renderer.new(Page.new({}, 'foo')) }
16
+ assert_nothing_raised(ArgumentError) { Renderer.new('foo', {}) }
17
+ end
18
+
19
+ should "raise an error when using the generic class" do
20
+ assert_raise(RuntimeError) { Renderer.new('foo', {}).render }
21
+ end
22
+ end
23
+
24
+ context 'when creating' do
25
+ should 'take a string and options' do
26
+ renderer = Renderer.new('foo', { :bar => 'bar' })
27
+ assert_equal('foo', renderer.string)
28
+ assert_equal({ :locals => { :bar => 'bar' }}, renderer.options)
29
+ end
30
+
31
+ should 'take a page as string and options' do
32
+ renderer = Renderer.new(Page.new({ :bar => 'bar'}, 'foo'))
33
+ assert_equal('foo', renderer.string)
34
+ assert_equal({ :locals => { :bar => 'bar' }}, renderer.options)
35
+ end
36
+ end
37
+
38
+ should "render a stylesheet" do
39
+ s = Stylesheet.new({ :filename => 'foo.css' }, 'foo')
40
+ x = Renderers::StylesheetRenderer.new(s)
41
+ x.expects(:render).returns('foo')
42
+ Renderers::StylesheetRenderer.expects(:new).at_least_once.returns(x)
43
+ Renderer.render(s)
44
+ end
45
+
46
+ should "render a javascript" do
47
+ s = Javascript.new({ :filename => 'foo.js' }, 'foo')
48
+ x = Renderers::JavascriptRenderer.new(s)
49
+ x.expects(:render).returns('foo')
50
+ Renderers::JavascriptRenderer.expects(:new).at_least_once.returns(x)
51
+ Renderer.render(s)
52
+ end
53
+
54
+ context "when rendering a page" do
55
+ setup do
56
+ @page = Page.new({ :layout => 'foo' }, 'bar')
57
+ @layout = Layout.new({}, 'layout: {{ yield }}')
58
+ Layout.expects(:find).with('foo').returns(@layout)
59
+ Layout.expects(:find).with(nil).returns(nil)
60
+ end
61
+
62
+ should "wrap in layout" do
63
+ assert_equal("layout: <p>bar</p>\n", Renderer.render(@page))
64
+ end
65
+
66
+ should "take extra locals" do
67
+ Renderers::PageRenderer.any_instance.expects(:liquify).with("layout: <p>bar</p>\n", { :layout => 'foo', :title => 'bla' }).returns('foo')
68
+ Renderer.render(@page, :title => 'bla')
69
+ end
70
+ end
71
+ end