diff --git a/TODO b/TODO index 7f09e444..f6cc6a89 100644 --- a/TODO +++ b/TODO @@ -6,6 +6,8 @@ Documentation: Features: Exceptions thrown by Sass code should have their own class + Exceptions in general should be a lot nicer Filters for Haml Haml and Sass should throw syntax errors rather than blithely parsing ill-formatted documents - There should be a way to represent options in-document + There should be a way to represent options in-document + Haml and Sass executables should return meaningful exit codes diff --git a/lib/haml.rb b/lib/haml.rb index b18573f2..412f9c0a 100644 --- a/lib/haml.rb +++ b/lib/haml.rb @@ -391,6 +391,59 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # probably make it | # multiline so it doesn't | # look awful. | +# +# ==== : +# +# The colon character designates a filter. +# This allows you to pass an indented block of text as input +# to another filtering program and add the result to the output of Haml. +# The syntax is simply a colon followed by the name of the filter. +# For example, +# +# %p +# :markdown +# Textile +# ======= +# +# Hello, *World* +# +# is compiled to +# +#

+#

Textile

+# +#

Hello, World

+#

+# +# Haml has the following filters defined: +# +# [plain] Does not parse the filtered text. +# +# [ruby] Parses the filtered text with the normal Ruby interpreter. +# All output sent to $stdout, like with +puts+, +# is output into the Haml document. +# Not available if the suppress_eval option is set to true. +# +# [erb] Parses the filtered text with ERB, like an RHTML template. +# Not available if the suppress_eval option is set to true. +# At the moment, this doesn't support access to variables +# defined by Ruby on Rails or Haml code. +# +# [sass] Parses the filtered text with Sass to produce CSS output. +# +# [redcloth] Parses the filtered text with RedCloth (http://whytheluckystiff.net/ruby/redcloth), +# which uses both Textile and Markdown syntax. +# Only works if RedCloth is installed. +# +# [textile] Parses the filtered text with Textile (http://www.textism.com/tools/textile). +# Only works if RedCloth is installed. +# +# [markdown] Parses the filtered text with Markdown (http://daringfireball.net/projects/markdown). +# Only works if RedCloth or BlueCloth (http://www.deveiate.org/projects/BlueCloth) +# is installed +# (BlueCloth takes precedence if both are installed). +# +# You can also define your own filters (see Setting Options, below). # # === Ruby evaluators # @@ -615,6 +668,18 @@ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir) # of this type within the attributes will be escaped # (e.g. by replacing them with ') if # the character is an apostrophe or a quotation mark. +# +# [:filters] A hash of filters that can be applied to Haml code. +# The keys are the string names of the filters; +# the values are references to the classes of the filters. +# User-defined filters should always have lowercase keys, +# and should have: +# * An +initialize+ method that accepts one parameter, +# the text to be filtered. +# * A +render+ method that returns the result of the filtering. +# * An optional haml_scope_object= method +# that takes a reference to the object +# that Ruby code in Haml is evaluated within. # # [:locals] The local variables that will be available within the # template. For instance, if :locals is diff --git a/lib/haml/buffer.rb b/lib/haml/buffer.rb index ed23a237..a1bfc384 100644 --- a/lib/haml/buffer.rb +++ b/lib/haml/buffer.rb @@ -203,22 +203,25 @@ module Haml end end -class String # :nodoc - alias_method :old_comp, :<=> - def <=>(other) - if other.is_a? NilClass - -1 - else - old_comp(other) +unless String.methods.include? 'old_comp' + class String # :nodoc + alias_method :old_comp, :<=> + + def <=>(other) + if other.is_a? NilClass + -1 + else + old_comp(other) + end + end + end + + class NilClass # :nodoc: + include Comparable + + def <=>(other) + other.nil? ? 0 : 1 end end end -class NilClass # :nodoc: - include Comparable - - def <=>(other) - other.nil? ? 0 : 1 - end -end - diff --git a/lib/haml/engine.rb b/lib/haml/engine.rb index 16a091a6..de59ea23 100644 --- a/lib/haml/engine.rb +++ b/lib/haml/engine.rb @@ -1,5 +1,6 @@ require 'haml/helpers' require 'haml/buffer' +require 'haml/filters' module Haml # This is the class where all the parsing and processing of the Haml @@ -47,6 +48,9 @@ module Haml # Designates a non-parsed line. ESCAPE = ?\\ + # Designates a block of filtered text. + FILTER = ?: + # Designates a non-parsed line. Not actually a character. PLAIN_TEXT = -1 @@ -61,7 +65,8 @@ module Haml SCRIPT, FLAT_SCRIPT, SILENT_SCRIPT, - ESCAPE + ESCAPE, + FILTER ] # The value of the character that designates that a line is part @@ -99,8 +104,32 @@ module Haml @options = { :suppress_eval => false, :attr_wrapper => "'", - :locals => {} - }.merge options + :locals => {}, + :filters => { + 'sass' => Sass::Engine, + 'plain' => Haml::Filters::Plain + } + } + + unless @options[:suppress_eval] + @options[:filters].merge!({ + 'erb' => ERB, + 'ruby' => Haml::Filters::Ruby + }) + end + + if !NOT_LOADED.include? 'redcloth' + @options[:filters].merge!({ + 'redcloth' => RedCloth, + 'textile' => Haml::Filters::Textile, + 'markdown' => Haml::Filters::Markdown + }) + elsif !NOT_LOADED.include? 'bluecloth' + @options[:filters]['markdown'] = Haml::Filters::Markdown + end + + @options.merge! options + @precompiled = @options[:precompiled] @template = template.strip #String @@ -155,7 +184,7 @@ module Haml old_tabs = nil (@template + "\n-#").each_with_index do |line, index| spaces, tabs = count_soft_tabs(line) - line = line.strip + line.strip! if !line.empty? if old_line @@ -181,9 +210,13 @@ module Haml old_spaces = spaces old_tabs = tabs elsif @flat_spaces != -1 - push_flat(old_line, old_spaces) - old_line = '' - old_spaces = 0 + process_indent(old_tabs, old_line) unless old_line.empty? + + if @flat_spaces != -1 + push_flat(old_line, old_spaces) + old_line = '' + old_spaces = 0 + end end end @@ -231,6 +264,9 @@ module Haml push_and_tabulate([:script]) end end + when FILTER + name = line[1..-1].downcase + start_filtered(options[:filters][name] || name) when DOCTYPE if line[0...3] == '!!!' render_doctype(line) @@ -358,7 +394,12 @@ module Haml # Adds +text+ to @buffer while flattening text. def push_flat(text, spaces) tabulation = spaces - @flat_spaces - @precompiled << "_hamlout.push_text(#{text.dump}, #{tabulation > -1 ? tabulation : 0}, true)\n" + tabulation = tabulation > -1 ? tabulation : 0 + if @filter_buffer + @filter_buffer << "#{' ' * tabulation}#{text}\n" + else + @precompiled << "_hamlout.push_text(#{text.dump}, #{tabulation}, true)\n" + end end # Causes text to be evaluated in the context of @@ -402,6 +443,8 @@ module Haml close_flat value when :loud close_loud value + when :filtered + close_filtered value end end @@ -444,6 +487,23 @@ module Haml @template_tabs -= 1 end + # Closes a filtered block. + def close_filtered(filter) + @flat_spaces = -1 + if filter.is_a? String + if filter == 'redcloth' || filter == 'markdown' || filter == 'textile' + push_text("You must have the RedCloth gem installed to use #{filter}") + else + push_text("Filter \"#{filter}\" is not defined!") + end + else + push_text(filter.new(@filter_buffer).render.rstrip.gsub("\n", "\n#{' ' * @output_tabs}")) + end + + @filter_buffer = nil + @template_tabs -= 1 + end + # Parses a line that will render as an XHTML tag, and adds the code that will # render that tag to @precompiled. def render_tag(line, index) @@ -541,6 +601,13 @@ module Haml @flat_spaces = @template_tabs * 2 end + # Starts a filtered block. + def start_filtered(filter) + push_and_tabulate([:filtered, filter]) + @flat_spaces = @template_tabs * 2 + @filter_buffer = String.new + end + # Counts the tabulation of a line. def count_soft_tabs(line) spaces = line.index(/[^ ]/) diff --git a/lib/haml/filters.rb b/lib/haml/filters.rb new file mode 100644 index 00000000..2e17b0b4 --- /dev/null +++ b/lib/haml/filters.rb @@ -0,0 +1,77 @@ +# This file contains redefinitions of and wrappers around various text +# filters so they can be used as Haml filters. + +# :stopdoc: + +require 'erb' +require 'sass/engine' +require 'stringio' + +volatile_requires = ['rubygems', 'redcloth', 'bluecloth'] +NOT_LOADED = [] +volatile_requires.each do |file| + begin + require file + rescue LoadError + NOT_LOADED.push file + end +end + +class ERB; alias_method :render, :result; end + +unless NOT_LOADED.include? 'bluecloth' + class BlueCloth; alias_method :render, :to_html; end +end + +module Haml + module Filters + class Plain + def initialize(text) + @text = text + end + + def render + @text + end + end + + class Ruby + def initialize(text) + @text = text + end + + def render + old_stdout = $stdout + $stdout = StringIO.new + Object.new.instance_eval(@text) + old_stdout, $stdout = $stdout, old_stdout + old_stdout.pos = 0 + old_stdout.read + end + end + + unless NOT_LOADED.include? 'redcloth' + class ::RedCloth; alias_method :render, :to_html; end + + # Uses RedCloth to provide only Textile (not Markdown) parsing + class Textile < RedCloth + def render + self.to_html(:textile) + end + end + + unless defined?(BlueCloth) + # Uses RedCloth to provide only Markdown (not Textile) parsing + class Markdown < RedCloth + def render + self.to_html(:markdown) + end + end + else + Markdown = BlueCloth + end + end + end +end + +# :startdoc: diff --git a/test/haml/results/filters.xhtml b/test/haml/results/filters.xhtml new file mode 100644 index 00000000..6d03b882 --- /dev/null +++ b/test/haml/results/filters.xhtml @@ -0,0 +1,48 @@ + +

Foo

+ + +
This is preformatted!
+Look at that!
+Wowie-zowie!
+ + +

boldilicious!

+This + Is + Plain + Text + %strong right? + + a + + b + + c + + d + + e + + f + + g + + h + + i + + j + +Text! +Hello, World! +How are you doing today? + diff --git a/test/haml/template_test.rb b/test/haml/template_test.rb index 3ec46874..0aa76108 100644 --- a/test/haml/template_test.rb +++ b/test/haml/template_test.rb @@ -12,7 +12,8 @@ require File.dirname(__FILE__) + '/mocks/article' class TemplateTest < Test::Unit::TestCase @@templates = %w{ very_basic standard helpers whitespace_handling original_engine list helpful - silent_script tag_parsing just_stuff partials } + silent_script tag_parsing just_stuff partials + filters } def setup ActionView::Base.register_template_handler("haml", Haml::Template) diff --git a/test/haml/templates/filters.haml b/test/haml/templates/filters.haml new file mode 100644 index 00000000..dfca5fe2 --- /dev/null +++ b/test/haml/templates/filters.haml @@ -0,0 +1,42 @@ +%style + :sass + p + :border + :style dotted + :width 10px + :color #ff00ff + h1 + :font-weight normal + +:redcloth + Foo + === + + This is preformatted! + Look at that! + Wowie-zowie! + + *boldilicious!* + +:plain + This + Is + Plain + Text + %strong right? + +:erb + <% 10.times do |c| %> + <%= (c+97).chr %> + <% end %> + +:markdown + * Foo + * Bar + * BAZ! + += "Text!" + +:ruby + puts "Hello, World!" + puts "How are you doing today?"