haml--haml/lib/haml/filters.rb

397 lines
14 KiB
Ruby

require "tilt"
module Haml
# The module containing the default Haml filters,
# as well as the base module, {Haml::Filters::Base}.
#
# @see Haml::Filters::Base
module Filters
extend self
# @return [{String => Haml::Filters::Base}] a hash mapping filter names to
# classes.
attr_reader :defined
@defined = {}
# Loads an external template engine from
# [Tilt](https://github.com/rtomayko/tilt) as a filter. This method is used
# internally by Haml to set up filters for Sass, SCSS, Less, Coffeescript,
# and others. It's left public to make it easy for developers to add their
# own Tilt-based filters if they choose.
#
# @return [Module] The generated filter.
# @param [Hash] options Options for generating the filter module.
# @option options [Boolean] :precompiled Whether the filter should be
# precompiled. Erb, Nokogiri and Builder use this, for example.
# @option options [Class] :template_class The Tilt template class to use,
# in the event it can't be inferred from an extension.
# @option options [String] :extension The extension associated with the
# content, for example "markdown". This lets Tilt choose the preferred
# engine when there are more than one.
# @option options [String,Array<String>] :alias Any aliases for the filter.
# For example, :coffee is also available as :coffeescript.
# @option options [String] :extend The name of a module to extend when
# defining the filter. Defaults to "Plain". This allows filters such as
# Coffee to "inherit" from Javascript, wrapping its output in script tags.
# @since 4.0
def register_tilt_filter(name, options = {})
if constants.map(&:to_s).include?(name.to_s)
raise "#{name} filter already defined"
end
filter = const_set(name, Module.new)
filter.extend const_get(options[:extend] || "Plain")
filter.extend TiltFilter
filter.extend PrecompiledTiltFilter if options.has_key? :precompiled
if options.has_key? :template_class
filter.template_class = options[:template_class]
else
filter.tilt_extension = options.fetch(:extension) { name.downcase }
end
# All ":coffeescript" as alias for ":coffee", etc.
if options.has_key?(:alias)
[options[:alias]].flatten.each {|x| Filters.defined[x.to_s] = filter}
end
filter
end
# Removes a filter from Haml. If the filter was removed, it returns
# the that was remove Module upon success, or nil on failure. If you try
# to redefine a filter, Haml will raise an error. Use this method first to
# explicitly remove the filter before redefining it.
# @return Module The filter module that has been removed
# @since 4.0
def remove_filter(name)
defined.delete name.to_s.downcase
if constants.map(&:to_s).include?(name.to_s)
remove_const name.to_sym
end
end
# The base module for Haml filters.
# User-defined filters should be modules including this module.
# The name of the filter is taken by downcasing the module name.
# For instance, if the module is named `FooBar`, the filter will be `:foobar`.
#
# A user-defined filter should override either \{#render} or {\#compile}.
# \{#render} is the most common.
# It takes a string, the filter source,
# and returns another string, the result of the filter.
# For example, the following will define a filter named `:sass`:
#
# module Haml::Filters::Sass
# include Haml::Filters::Base
#
# def render(text)
# ::Sass::Engine.new(text).render
# end
# end
#
# For details on overriding \{#compile}, see its documentation.
#
# Note that filters overriding \{#render} automatically support `#{}`
# for interpolating Ruby code.
# Those overriding \{#compile} will need to add such support manually
# if it's desired.
module Base
# This method is automatically called when {Base} is included in a module.
# It automatically defines a filter
# with the downcased name of that module.
# For example, if the module is named `FooBar`, the filter will be `:foobar`.
#
# @param base [Module, Class] The module that this is included in
def self.included(base)
Filters.defined[base.name.split("::").last.downcase] = base
base.extend(base)
end
# Takes the source text that should be passed to the filter
# and returns the result of running the filter on that string.
#
# This should be overridden in most individual filter modules
# to render text with the given filter.
# If \{#compile} is overridden, however, \{#render} doesn't need to be.
#
# @param text [String] The source text for the filter to process
# @return [String] The filtered result
# @raise [Haml::Error] if it's not overridden
def render(text)
raise Error.new("#{self.inspect}#render not defined!")
end
# Same as \{#render}, but takes a {Haml::Engine} options hash as well.
# It's only safe to rely on options made available in {Haml::Engine#options\_for\_buffer}.
#
# @see #render
# @param text [String] The source text for the filter to process
# @return [String] The filtered result
# @raise [Haml::Error] if it or \{#render} isn't overridden
def render_with_options(text, options)
render(text)
end
# Same as \{#compile}, but requires the necessary files first.
# *This is used by {Haml::Engine} and is not intended to be overridden or used elsewhere.*
#
# @see #compile
def internal_compile(*args)
compile(*args)
end
# This should be overridden when a filter needs to have access to the Haml
# evaluation context. Rather than applying a filter to a string at
# compile-time, \{#compile} uses the {Haml::Compiler} instance to compile
# the string to Ruby code that will be executed in the context of the
# active Haml template.
#
# Warning: the {Haml::Compiler} interface is neither well-documented
# nor guaranteed to be stable.
# If you want to make use of it, you'll probably need to look at the
# source code and should test your filter when upgrading to new Haml
# versions.
#
# @param compiler [Haml::Compiler] The compiler instance
# @param text [String] The text of the filter
# @raise [Haml::Error] if none of \{#compile}, \{#render}, and
# \{#render_with_options} are overridden
def compile(compiler, text)
filter = self
compiler.instance_eval do
if contains_interpolation?(text)
return if options[:suppress_eval]
text = unescape_interpolation(text).gsub(/(\\+)n/) do |s|
escapes = $1.size
next s if escapes % 2 == 0
"#{'\\' * (escapes - 1)}\n"
end
# We need to add a newline at the beginning to get the
# filter lines to line up (since the Haml filter contains
# a line that doesn't show up in the source, namely the
# filter name). Then we need to escape the trailing
# newline so that the whole filter block doesn't take up
# too many.
text = %[\n#{text.sub(/\n"\Z/, "\\n\"")}]
push_script <<RUBY.rstrip, :escape_html => false
find_and_preserve(#{filter.inspect}.render_with_options(#{text}, _hamlout.options))
RUBY
return
end
rendered = Haml::Helpers::find_and_preserve(filter.render_with_options(text, compiler.options), compiler.options[:preserve])
rendered.rstrip!
rendered.gsub!("\n", "\n#{' ' * @output_tabs}") unless options[:ugly]
push_text(rendered)
end
end
end
# Does not parse the filtered text.
# This is useful for large blocks of text without HTML tags, when you don't
# want lines starting with `.` or `-` to be parsed.
module Plain
include Base
# @see Base#render
def render(text); text; end
end
# Surrounds the filtered text with `<script>` and CDATA tags. Useful for
# including inline Javascript.
module Javascript
include Base
# @see Base#render_with_options
def render_with_options(text, options)
indent = options[:cdata] ? ' ' : ' ' # 4 or 2 spaces
if options[:format] == :html5
type = ''
else
type = " type=#{options[:attr_wrapper]}text/javascript#{options[:attr_wrapper]}"
end
text = text.rstrip
text.gsub!("\n", "\n#{indent}")
%!<script#{type}>\n#{" //<![CDATA[\n" if options[:cdata]}#{indent}#{text}\n#{" //]]>\n" if options[:cdata]}</script>!
end
end
# Surrounds the filtered text with `<style>` and CDATA tags. Useful for
# including inline CSS.
module Css
include Base
# @see Base#render_with_options
def render_with_options(text, options)
indent = options[:cdata] ? ' ' : ' ' # 4 or 2 spaces
if options[:format] == :html5
type = ''
else
type = " type=#{options[:attr_wrapper]}text/css#{options[:attr_wrapper]}"
end
text = text.rstrip
text.gsub!("\n", "\n#{indent}")
%(<style#{type}>\n#{" /*<![CDATA[*/\n" if options[:cdata]}#{indent}#{text}\n#{" /*]]>*/\n" if options[:cdata]}</style>)
end
end
# Surrounds the filtered text with CDATA tags.
module Cdata
include Base
# @see Base#render
def render(text)
text = "\n#{text}"
text.rstrip!
text.gsub!("\n", "\n ")
"<![CDATA[#{text}\n]]>"
end
end
# Works the same as {Plain}, but HTML-escapes the text before placing it in
# the document.
module Escaped
include Base
# @see Base#render
def render(text)
Haml::Helpers.html_escape text
end
end
# Parses the filtered text with the normal Ruby interpreter. Creates an IO
# object named `haml_io`, anything written to it is output into the Haml
# document. In previous version this filter redirected any output to `$stdout`
# to the Haml document, this was not threadsafe and has been removed, you
# should use `haml_io` instead.
#
# Not available if the {file:REFERENCE.md#suppress_eval-option `:suppress_eval`}
# option is set to true. The Ruby code is evaluated in the same context as
# the Haml template.
module Ruby
include Base
require 'stringio'
# @see Base#compile
def compile(compiler, text)
return if compiler.options[:suppress_eval]
compiler.instance_eval do
push_silent <<-FIRST.tr("\n", ';') + text + <<-LAST.tr("\n", ';')
begin
haml_io = StringIO.new(_hamlout.buffer, 'a')
FIRST
ensure
haml_io.close
haml_io = nil
end
LAST
end
end
end
# Inserts the filtered text into the template with whitespace preserved.
# `preserve`d blocks of text aren't indented, and newlines are replaced with
# the HTML escape code for newlines, to preserve nice-looking output.
#
# @see Haml::Helpers#preserve
module Preserve
include Base
# @see Base#render
def render(text)
Haml::Helpers.preserve text
end
end
# @private
module TiltFilter
extend self
attr_accessor :tilt_extension, :options
attr_writer :template_class
def template_class
(@template_class if defined? @template_class) or begin
@template_class = Tilt["t.#{tilt_extension}"] or
raise Error.new(Error.message(:cant_run_filter, tilt_extension))
rescue LoadError => e
dep = e.message.split('--').last.strip
raise Error.new(Error.message(:gem_install_filter_deps, tilt_extension, dep))
end
end
def self.extended(base)
base.options = {}
# There's a bug in 1.9.2 where the same parse tree cannot be shared
# across several singleton classes -- this bug is fixed in 1.9.3.
# We work around this by using a string eval instead of a block eval
# so that a new parse tree is created for each singleton class.
base.instance_eval %Q{
include Base
def render_with_options(text, compiler_options)
text = template_class.new(nil, 1, options) {text}.render
super(text, compiler_options)
end
}
end
end
# @private
module PrecompiledTiltFilter
def precompiled(text)
template_class.new(nil, 1, options) { text }.send(:precompiled, {}).first
end
def compile(compiler, text)
return if compiler.options[:suppress_eval]
compiler.send(:push_script, precompiled(text))
end
end
# @!parse module Sass; end
register_tilt_filter "Sass", :extend => "Css"
# @!parse module Scss; end
register_tilt_filter "Scss", :extend => "Css"
# @!parse module Less; end
register_tilt_filter "Less", :extend => "Css"
# @!parse module Markdown; end
register_tilt_filter "Markdown"
# @!parse module Erb; end
register_tilt_filter "Erb", :precompiled => true
# @!parse module Coffee; end
register_tilt_filter "Coffee", :alias => "coffeescript", :extend => "Javascript"
# Parses the filtered text with ERB.
# Not available if the {file:REFERENCE.md#suppress_eval-option
# `:suppress_eval`} option is set to true. Embedded Ruby code is evaluated
# in the same context as the Haml template.
module Erb
class << self
def precompiled(text)
super.sub(/^#coding:.*?\n/, '')
end
end
end
end
end
# These filters have been demoted to Haml Contrib but are still included by
# default in Haml 4.0. Still, we rescue from load error if for some reason
# haml-contrib is not installed.
begin
require "haml/filters/maruku"
require "haml/filters/textile"
rescue LoadError
end