diff --git a/haml-contrib.gemspec b/haml-contrib.gemspec index e6f20dc..bc729a7 100644 --- a/haml-contrib.gemspec +++ b/haml-contrib.gemspec @@ -12,5 +12,6 @@ Gem::Specification.new do |spec| spec.add_dependency "haml", ">= 3.2.0.alpha.13" spec.add_development_dependency "minitest" + spec.add_development_dependency "nokogiri" end diff --git a/lib/haml/layouts.rb b/lib/haml/layouts.rb new file mode 100644 index 0000000..2033b26 --- /dev/null +++ b/lib/haml/layouts.rb @@ -0,0 +1,53 @@ +require 'haml' +require 'haml/options_ext' + +module Haml + + Options.add_option :layout_base_dir, Dir.getwd + Options.add_option :layout, 'layout.haml' + + class Engine + + # TODO: work out the best place to put this documentation. + + # A simple layouts implementation. + # + # Renders the Haml template as normal, then renders the file specified + # by +options.layout+ (defaults to +layout.haml+) and inserts the original + # rendered file where +yield+ is called in the layout. + # + # Can also handle +content_for :sym+ blocks which will be inserted in the + # layout when +yield :sym+ is called, similar to Rails. + # + # To specify a different directory to look for layout files use the + # +layout_base_dir+ option, the default is the current working directory. + def render_with_layout(scope = Object.new, locals = {}, &block) + return render_without_layout(scope, locals, &block) unless options[:layout] + + regions = {} + scope.instance_variable_set '@layout_regions', regions + + layout_file = File.expand_path(options.layout, options.layout_base_dir) + options.layout = nil + + unnamed = render_without_layout(scope, locals) + + Haml::Engine.new(File.read(layout_file), options.to_hash).render_without_layout(scope, locals) do |*region| + region[0] ? regions[region[0]] : unnamed + end + end + alias :render_without_layout :render + alias :render :render_with_layout + alias :to_html :render_with_layout + + end + + module Helpers + + def content_for(region, &blk) + @layout_regions[region] = capture_haml(&blk) + end + + end + +end \ No newline at end of file diff --git a/lib/haml/options_ext.rb b/lib/haml/options_ext.rb new file mode 100644 index 0000000..3a9787c --- /dev/null +++ b/lib/haml/options_ext.rb @@ -0,0 +1,37 @@ +require 'haml/options' + +module Haml + + # @private + class Options + + def to_hash + self.class.defaults.keys.inject({}) do |hash, key| + hash[key] = send(key) + hash + end + end + + def self.add_option(name, default, for_buffer=false) + @defaults[name] = default + attr_accessor name + @buffer_option_keys << name if for_buffer + end + + end + + class Engine + + def render_with_options(scope = Object.new, locals = {}, &block) + # We sometimes need to be able to get the original options when making later + # calls to render (e.g. when rendering partials) so we "hide" them in the scope + # object. (We can't just use the buffer options as we may need _all_ the options.) + scope.instance_variable_set '@_original_options', options unless scope.instance_variable_get '@_original_options' + render_without_options(scope, locals, &block) + end + alias :render_without_options :render + alias :render :render_with_options + alias :to_html :render_with_options + end + +end \ No newline at end of file diff --git a/lib/haml/partials.rb b/lib/haml/partials.rb new file mode 100644 index 0000000..198dc60 --- /dev/null +++ b/lib/haml/partials.rb @@ -0,0 +1,52 @@ +require 'haml' +require 'haml/options_ext' + +module Haml + + Options.add_option :partial_base_dir, Dir.getwd, true + + module Helpers + + # A simple implementation of partials. + # + # Renders the named Haml file and returns the generated HTML. + # + # The directory to search for partials in can be set with the + # +:partial_base_dir+ option, which defaults to the current working directory. + # + # If the named file is not found, the suffixes +.haml+ and +.html.haml+ are + # added and looked for, and then the same names but with the prefix +_+. The + # first file found will be used. + # + # +self+ is used as the context, so instance variables can be used in the + # partial and will have the same value as the parent template. + # + # This method makes no effort to cache the generated Ruby code, it simply + # uses +Haml::Engine.new().render+ each time. This implementation is intended + # for static site generators or one off document generation, and not as a + # base for a web framework. + # + # @param partial [#to_s] the filename of the partial to render + # @param locals [Hash] a hash of local variables to use in the partial + # @return [String] the HTML generated from the partial + # @raise [StandardError] if the partial file cannot be found + + def render(partial, locals = {}) + Haml::Engine.new(File.read(find_file_for_partial(partial)), @_original_options.to_hash).render(self, locals) + end + + private + def find_file_for_partial(file) + ['', '_'].each do |prefix| + ['', '.haml', '.html.haml'].each do |suffix| + candidate = File.expand_path "#{prefix}#{file}#{suffix}", haml_buffer.options[:partial_base_dir] + return candidate if File.exist? candidate + end + end + + raise "Partial #{file} not found" + end + + end + +end \ No newline at end of file diff --git a/test/layouts/basic.haml b/test/layouts/basic.haml new file mode 100644 index 0000000..09adbb5 --- /dev/null +++ b/test/layouts/basic.haml @@ -0,0 +1,5 @@ +-content_for :title do + Hello + +%p + Here’s some content. diff --git a/test/layouts/basic_content.haml b/test/layouts/basic_content.haml new file mode 100644 index 0000000..9f65199 --- /dev/null +++ b/test/layouts/basic_content.haml @@ -0,0 +1 @@ +%p.content \ No newline at end of file diff --git a/test/layouts/basic_layout.haml b/test/layouts/basic_layout.haml new file mode 100644 index 0000000..15dee95 --- /dev/null +++ b/test/layouts/basic_layout.haml @@ -0,0 +1,2 @@ +%div.layout + =yield \ No newline at end of file diff --git a/test/layouts/content_for_content.haml b/test/layouts/content_for_content.haml new file mode 100644 index 0000000..ec12fbe --- /dev/null +++ b/test/layouts/content_for_content.haml @@ -0,0 +1,4 @@ +- content_for :region do + %span.region Region + +%p.content Content \ No newline at end of file diff --git a/test/layouts/content_for_layout.haml b/test/layouts/content_for_layout.haml new file mode 100644 index 0000000..8a396d9 --- /dev/null +++ b/test/layouts/content_for_layout.haml @@ -0,0 +1,4 @@ +.content_for + =yield :region + +=yield \ No newline at end of file diff --git a/test/layouts/layout.haml b/test/layouts/layout.haml new file mode 100644 index 0000000..03301b1 --- /dev/null +++ b/test/layouts/layout.haml @@ -0,0 +1,6 @@ +!!! +%html + %head + %title= yield :title + %body + = yield diff --git a/test/layouts/layout_uses_same_options_content.haml b/test/layouts/layout_uses_same_options_content.haml new file mode 100644 index 0000000..38ffe57 --- /dev/null +++ b/test/layouts/layout_uses_same_options_content.haml @@ -0,0 +1,2 @@ +.content + Content \ No newline at end of file diff --git a/test/layouts/layout_uses_same_options_layout.haml b/test/layouts/layout_uses_same_options_layout.haml new file mode 100644 index 0000000..fd42eba --- /dev/null +++ b/test/layouts/layout_uses_same_options_layout.haml @@ -0,0 +1,2 @@ +.layout + =yield \ No newline at end of file diff --git a/test/layouts_and_partials/basic_layout_with_partial_content.haml b/test/layouts_and_partials/basic_layout_with_partial_content.haml new file mode 100644 index 0000000..5b4d936 --- /dev/null +++ b/test/layouts_and_partials/basic_layout_with_partial_content.haml @@ -0,0 +1,2 @@ +.content + =render 'basic_layout_with_partial_partial' \ No newline at end of file diff --git a/test/layouts_and_partials/basic_layout_with_partial_layout.haml b/test/layouts_and_partials/basic_layout_with_partial_layout.haml new file mode 100644 index 0000000..fd42eba --- /dev/null +++ b/test/layouts_and_partials/basic_layout_with_partial_layout.haml @@ -0,0 +1,2 @@ +.layout + =yield \ No newline at end of file diff --git a/test/layouts_and_partials/basic_layout_with_partial_partial.haml b/test/layouts_and_partials/basic_layout_with_partial_partial.haml new file mode 100644 index 0000000..2d7df20 --- /dev/null +++ b/test/layouts_and_partials/basic_layout_with_partial_partial.haml @@ -0,0 +1 @@ +.partial \ No newline at end of file diff --git a/test/layouts_and_partials/layout_has_partial_content.haml b/test/layouts_and_partials/layout_has_partial_content.haml new file mode 100644 index 0000000..8127e10 --- /dev/null +++ b/test/layouts_and_partials/layout_has_partial_content.haml @@ -0,0 +1 @@ +.content Content \ No newline at end of file diff --git a/test/layouts_and_partials/layout_has_partial_layout.haml b/test/layouts_and_partials/layout_has_partial_layout.haml new file mode 100644 index 0000000..45c8906 --- /dev/null +++ b/test/layouts_and_partials/layout_has_partial_layout.haml @@ -0,0 +1,6 @@ +.layout_partial_container + =render 'layout_has_partial_partial' + +.layout + =yield + diff --git a/test/layouts_and_partials/layout_has_partial_partial.haml b/test/layouts_and_partials/layout_has_partial_partial.haml new file mode 100644 index 0000000..e8b89e4 --- /dev/null +++ b/test/layouts_and_partials/layout_has_partial_partial.haml @@ -0,0 +1 @@ +.partial Partial \ No newline at end of file diff --git a/test/layouts_and_partials_test.rb b/test/layouts_and_partials_test.rb new file mode 100644 index 0000000..72c47b0 --- /dev/null +++ b/test/layouts_and_partials_test.rb @@ -0,0 +1,27 @@ +require "test_helper" +require "haml/layouts" +Haml::Options.defaults[:layout] = nil # turn off layouts so they don't affect other tests +require "haml/partials" + +class LayoutsPartialsTest < Minitest::Unit::TestCase + + def render(file, options = {}) + base_dir = File.expand_path('../layouts_and_partials', __FILE__) + super File.read(File.expand_path(file, base_dir)), options.merge(:layout_base_dir => base_dir, :partial_base_dir => base_dir) + end + + def test_basic_partial_and_layout + html = render('basic_layout_with_partial_content.haml', :layout => 'basic_layout_with_partial_layout.haml') + + assert_css '.layout > .content > .partial', html + refute_css '.layout > .content > .layout > .partial', html # partial should not have the layout + end + + def test_layout_can_have_partial + html = render('layout_has_partial_content.haml', :layout => 'layout_has_partial_layout.haml') + + assert_css '.layout_partial_container > .partial', html + assert_css '.layout > .content', html + end + +end \ No newline at end of file diff --git a/test/layouts_test.rb b/test/layouts_test.rb new file mode 100644 index 0000000..695c3a1 --- /dev/null +++ b/test/layouts_test.rb @@ -0,0 +1,38 @@ +require "test_helper" +require "haml/layouts" +Haml::Options.defaults[:layout] = nil # turn off layouts so they don't affect other tests + +class LayoutTest < Minitest::Unit::TestCase + + def render(file, options = {}) + base_dir = File.expand_path('../layouts', __FILE__) + super File.read(File.expand_path(file, base_dir)), options.merge(:layout_base_dir => base_dir) + end + + def test_basic_layout + html = render('basic_content.haml', :layout => 'basic_layout.haml') + + assert_match /class='content'/, html # content is rendered + assert_match /class='layout'/, html # layout is rendered + end + + def test_content_for + html = render('content_for_content.haml', :layout => 'content_for_layout.haml') + + assert_css('div.content_for > span.region', html) + assert_css('p.content', html) + end + + def test_layout_uses_same_options + html = render('layout_uses_same_options_content.haml', + :layout => 'layout_uses_same_options_layout.haml', + :attr_wrapper => '"', :remove_whitespace => true) + + assert_match /class="layout"/, html + assert_match /class="content"/, html + + assert_match /class="layout">