From 2fa28d63e3205708a87ab18060c8bc9f2b3fbd02 Mon Sep 17 00:00:00 2001
From: nex3
Date: Fri, 19 Jan 2007 17:03:47 +0000
Subject: [PATCH] Filters: implemented!
git-svn-id: svn://hamptoncatlin.com/haml/trunk@291 7063305b-7217-0410-af8c-cdc13e5119b9
---
TODO | 4 +-
lib/haml.rb | 65 +++++++++++++++++++++++++
lib/haml/buffer.rb | 33 +++++++------
lib/haml/engine.rb | 83 +++++++++++++++++++++++++++++---
lib/haml/filters.rb | 77 +++++++++++++++++++++++++++++
test/haml/results/filters.xhtml | 48 ++++++++++++++++++
test/haml/template_test.rb | 3 +-
test/haml/templates/filters.haml | 42 ++++++++++++++++
8 files changed, 330 insertions(+), 25 deletions(-)
create mode 100644 lib/haml/filters.rb
create mode 100644 test/haml/results/filters.xhtml
create mode 100644 test/haml/templates/filters.haml
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?"