haml 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of haml might be problematic. Click here for more details.
- data/MIT-LICENSE +20 -0
- data/REFERENCE +662 -0
- data/Rakefile +167 -0
- data/VERSION +1 -0
- data/bin/haml +18 -0
- data/lib/haml/buffer.rb +224 -0
- data/lib/haml/engine.rb +551 -0
- data/lib/haml/helpers.rb +220 -0
- data/lib/haml/helpers/action_view_mods.rb +53 -0
- data/lib/haml/template.rb +138 -0
- data/test/benchmark.rb +62 -0
- data/test/engine_test.rb +93 -0
- data/test/helper_test.rb +105 -0
- data/test/mocks/article.rb +6 -0
- data/test/profile.rb +45 -0
- data/test/results/content_for_layout.xhtml +16 -0
- data/test/results/eval_suppressed.xhtml +2 -0
- data/test/results/helpers.xhtml +50 -0
- data/test/results/helpful.xhtml +5 -0
- data/test/results/just_stuff.xhtml +36 -0
- data/test/results/list.xhtml +12 -0
- data/test/results/original_engine.xhtml +24 -0
- data/test/results/partials.xhtml +20 -0
- data/test/results/silent_script.xhtml +74 -0
- data/test/results/standard.xhtml +42 -0
- data/test/results/tag_parsing.xhtml +28 -0
- data/test/results/very_basic.xhtml +7 -0
- data/test/results/whitespace_handling.xhtml +51 -0
- data/test/rhtml/standard.rhtml +51 -0
- data/test/runner.rb +15 -0
- data/test/template_test.rb +137 -0
- data/test/templates/_partial.haml +7 -0
- data/test/templates/_text_area.haml +3 -0
- data/test/templates/content_for_layout.haml +10 -0
- data/test/templates/eval_suppressed.haml +5 -0
- data/test/templates/helpers.haml +39 -0
- data/test/templates/helpful.haml +6 -0
- data/test/templates/just_stuff.haml +29 -0
- data/test/templates/list.haml +12 -0
- data/test/templates/original_engine.haml +17 -0
- data/test/templates/partialize.haml +1 -0
- data/test/templates/partials.haml +12 -0
- data/test/templates/silent_script.haml +40 -0
- data/test/templates/standard.haml +40 -0
- data/test/templates/tag_parsing.haml +24 -0
- data/test/templates/very_basic.haml +4 -0
- data/test/templates/whitespace_handling.haml +66 -0
- metadata +108 -0
data/Rakefile
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
volatile_requires = ['rcov/rcovtask']
|
5
|
+
not_loaded = []
|
6
|
+
volatile_requires.each do |file|
|
7
|
+
begin
|
8
|
+
require file
|
9
|
+
rescue LoadError
|
10
|
+
not_loaded.push file
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# For some crazy reason,
|
15
|
+
# some Rake tasks interfere with others
|
16
|
+
# (specifically, benchmarking).
|
17
|
+
# Thus, it's advantageous to only show
|
18
|
+
# the task currently being used.
|
19
|
+
def is_task?(*tasks)
|
20
|
+
ARGV[0].nil? || tasks.include?(ARGV[0])
|
21
|
+
end
|
22
|
+
|
23
|
+
# ----- Default: Testing ------
|
24
|
+
|
25
|
+
desc 'Default: run unit tests.'
|
26
|
+
task :default => :test
|
27
|
+
|
28
|
+
if is_task?('test', 'default')
|
29
|
+
require 'rake/testtask'
|
30
|
+
|
31
|
+
desc 'Test the Haml plugin'
|
32
|
+
Rake::TestTask.new(:test) do |t|
|
33
|
+
t.libs << 'lib'
|
34
|
+
t.pattern = 'test/**/*_test.rb'
|
35
|
+
t.verbose = true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# ----- Packaging -----
|
40
|
+
|
41
|
+
if is_task?('package', 'repackage', 'clobber_package')
|
42
|
+
require 'rake/gempackagetask'
|
43
|
+
|
44
|
+
spec = Gem::Specification.new do |spec|
|
45
|
+
spec.name = 'haml'
|
46
|
+
spec.summary = 'An elegant, structured XHTML/XML templating engine.'
|
47
|
+
spec.version = File.read('VERSION').strip
|
48
|
+
spec.author = 'Hampton Catlin'
|
49
|
+
spec.email = 'haml@googlegroups.com'
|
50
|
+
spec.description = <<-END
|
51
|
+
Haml (HTML Abstraction Markup Language) is a layer on top of XHTML or XML
|
52
|
+
that's designed to express the structure of XHTML or XML documents
|
53
|
+
in a non-repetitive, elegant, easy way,
|
54
|
+
using indentation rather than closing tags
|
55
|
+
and allowing Ruby to be embedded with ease.
|
56
|
+
It was originally envisioned as a plugin for Ruby on Rails,
|
57
|
+
but it can function as a stand-alone templating engine.
|
58
|
+
END
|
59
|
+
|
60
|
+
readmes = FileList.new('*') { |list| list.exclude(/[a-z]/) }.to_a
|
61
|
+
spec.executables = ['haml']
|
62
|
+
spec.files = FileList['lib/**/*', 'bin/*', 'test/**/*', 'Rakefile'].to_a + readmes
|
63
|
+
spec.homepage = 'http://haml.hamptoncatlin.com/'
|
64
|
+
spec.has_rdoc = true
|
65
|
+
spec.extra_rdoc_files = readmes
|
66
|
+
spec.rdoc_options += [
|
67
|
+
'--title', 'Haml',
|
68
|
+
'--main', 'REFERENCE',
|
69
|
+
'--exclude', 'lib/haml/buffer.rb',
|
70
|
+
'--line-numbers',
|
71
|
+
'--inline-source'
|
72
|
+
]
|
73
|
+
spec.test_files = FileList['test/**/*_test.rb'].to_a
|
74
|
+
end
|
75
|
+
|
76
|
+
Rake::GemPackageTask.new(spec) { |pkg| }
|
77
|
+
end
|
78
|
+
|
79
|
+
# ----- Benchmarking -----
|
80
|
+
|
81
|
+
if is_task?('benchmark')
|
82
|
+
temp_desc = <<END
|
83
|
+
Benchmark HAML against ERb.
|
84
|
+
TIMES=n sets the number of runs. Defaults to 100.
|
85
|
+
END
|
86
|
+
|
87
|
+
desc temp_desc.chomp
|
88
|
+
task :benchmark do
|
89
|
+
require 'test/benchmark'
|
90
|
+
|
91
|
+
puts '-'*51, "Benchmark: Haml vs. ERb", '-'*51
|
92
|
+
puts "Running benchmark #{ENV['TIMES']} times..." if ENV['TIMES']
|
93
|
+
times = ENV['TIMES'].to_i if ENV['TIMES']
|
94
|
+
benchmarker = Haml::Benchmarker.new
|
95
|
+
puts benchmarker.benchmark(times || 100)
|
96
|
+
puts '-'*51
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# ----- Documentation -----
|
101
|
+
|
102
|
+
if is_task?('rdoc', 'rerdoc', 'clobber_rdoc', 'rdoc_devel', 'rerdoc_devel', 'clobber_rdoc_devel')
|
103
|
+
require 'rake/rdoctask'
|
104
|
+
|
105
|
+
rdoc_task = Proc.new do |rdoc|
|
106
|
+
rdoc.title = 'Haml'
|
107
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
108
|
+
rdoc.rdoc_files.include('REFERENCE')
|
109
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
110
|
+
rdoc.rdoc_files.exclude('lib/haml/buffer.rb')
|
111
|
+
end
|
112
|
+
|
113
|
+
Rake::RDocTask.new do |rdoc|
|
114
|
+
rdoc_task.call(rdoc)
|
115
|
+
rdoc.rdoc_dir = 'rdoc'
|
116
|
+
end
|
117
|
+
|
118
|
+
Rake::RDocTask.new(:rdoc_devel) do |rdoc|
|
119
|
+
rdoc_task.call(rdoc)
|
120
|
+
rdoc.rdoc_dir = 'rdoc_devel'
|
121
|
+
rdoc.options << '--all'
|
122
|
+
rdoc.rdoc_files.include('test/*.rb')
|
123
|
+
rdoc.rdoc_files = Rake::FileList.new(*rdoc.rdoc_files.to_a)
|
124
|
+
rdoc.rdoc_files.include('lib/haml/buffer.rb')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# ----- Coverage -----
|
129
|
+
|
130
|
+
if is_task?('rcov', 'clobber_rcov')
|
131
|
+
unless not_loaded.include? 'rcov/rcovtask'
|
132
|
+
Rcov::RcovTask.new do |t|
|
133
|
+
t.libs << "test"
|
134
|
+
t.test_files = FileList['test/**/*_test.rb']
|
135
|
+
if ENV['NON_NATIVE']
|
136
|
+
t.rcov_opts << "--no-rcovrt"
|
137
|
+
end
|
138
|
+
t.verbose = true
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# ----- Profiling -----
|
144
|
+
|
145
|
+
if is_task?('profile')
|
146
|
+
temp_desc = <<END
|
147
|
+
Run a profile of HAML.
|
148
|
+
TIMES=n sets the number of runs. Defaults to 100.
|
149
|
+
FILE=n sets the file to profile. Defaults to 'standard'.
|
150
|
+
END
|
151
|
+
desc temp_desc.chomp
|
152
|
+
task :profile do
|
153
|
+
require 'test/profile'
|
154
|
+
|
155
|
+
puts '-'*51, "Profiling Haml::Template", '-'*51
|
156
|
+
|
157
|
+
args = []
|
158
|
+
args.push ENV['TIMES'].to_i if ENV['TIMES']
|
159
|
+
args.push ENV['FILE'] if ENV['FILE']
|
160
|
+
|
161
|
+
profiler = Haml::Profiler.new
|
162
|
+
res = profiler.profile(*args)
|
163
|
+
puts res
|
164
|
+
|
165
|
+
puts '-'*51
|
166
|
+
end
|
167
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/bin/haml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# The command line Haml parser.
|
3
|
+
|
4
|
+
if ARGV[0] == "--help" or ARGV[0] == "-h" or ARGV[0] == "-?"
|
5
|
+
puts <<END
|
6
|
+
Usage: haml (template file) (output file)
|
7
|
+
|
8
|
+
Description:
|
9
|
+
Uses the Haml engine to parse the specified template
|
10
|
+
and outputs the result to the specified file.
|
11
|
+
END
|
12
|
+
else
|
13
|
+
require File.join(File.dirname(__FILE__), '..', 'lib', 'haml', 'engine')
|
14
|
+
|
15
|
+
template = File.read(ARGV[0])
|
16
|
+
result = Haml::Engine.new(template).to_html
|
17
|
+
File.open(ARGV[1], "w") { |f| f.write(result) }
|
18
|
+
end
|
data/lib/haml/buffer.rb
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
module Haml
|
2
|
+
# This class is used only internally. It holds the buffer of XHTML that
|
3
|
+
# is eventually output by Haml::Engine's to_html method. It's called
|
4
|
+
# from within the precompiled code, and helps reduce the amount of
|
5
|
+
# processing done within instance_eval'd code.
|
6
|
+
class Buffer
|
7
|
+
include Haml::Helpers
|
8
|
+
|
9
|
+
# Set the maximum length for a line to be considered a one-liner.
|
10
|
+
# Lines <= the maximum will be rendered on one line,
|
11
|
+
# i.e. <tt><p>Hello world</p></tt>
|
12
|
+
ONE_LINER_LENGTH = 50
|
13
|
+
|
14
|
+
# The string that holds the compiled XHTML. This is aliased as
|
15
|
+
# _erbout for compatibility with ERB-specific code.
|
16
|
+
attr_accessor :buffer
|
17
|
+
|
18
|
+
# The number of tabs that are added or subtracted from the
|
19
|
+
# tabulation proscribed by the precompiled template.
|
20
|
+
attr_accessor :tabulation
|
21
|
+
|
22
|
+
# Creates a new buffer.
|
23
|
+
def initialize(options = {})
|
24
|
+
@options = options
|
25
|
+
@quote_escape = options[:attr_wrapper] == '"' ? """ : "'"
|
26
|
+
@other_quote_char = options[:attr_wrapper] == '"' ? "'" : '"'
|
27
|
+
@buffer = ""
|
28
|
+
@one_liner_pending = false
|
29
|
+
@tabulation = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
# Renders +text+ with the proper tabulation. This also deals with
|
33
|
+
# making a possible one-line tag one line or not.
|
34
|
+
def push_text(text, tabulation, flattened = false)
|
35
|
+
if flattened
|
36
|
+
# In this case, tabulation is the number of spaces, rather
|
37
|
+
# than the number of tabs.
|
38
|
+
@buffer << "#{' ' * tabulation}#{flatten(text + "\n")}"
|
39
|
+
@one_liner_pending = true
|
40
|
+
elsif @one_liner_pending && one_liner?(text)
|
41
|
+
@buffer << text
|
42
|
+
else
|
43
|
+
if @one_liner_pending
|
44
|
+
@buffer << "\n"
|
45
|
+
@one_liner_pending = false
|
46
|
+
end
|
47
|
+
@buffer << "#{tabs(tabulation)}#{text}\n"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Properly formats the output of a script that was run in the
|
52
|
+
# instance_eval.
|
53
|
+
def push_script(result, tabulation, flattened)
|
54
|
+
if flattened
|
55
|
+
result = find_and_flatten(result)
|
56
|
+
end
|
57
|
+
unless result.nil?
|
58
|
+
result = result.to_s
|
59
|
+
while result[-1] == 10 # \n
|
60
|
+
# String#chomp is slow
|
61
|
+
result = result[0...-1]
|
62
|
+
end
|
63
|
+
|
64
|
+
result = result.gsub("\n", "\n#{tabs(tabulation)}")
|
65
|
+
push_text result, tabulation
|
66
|
+
end
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
# Takes the various information about the opening tag for an
|
71
|
+
# element, formats it, and adds it to the buffer.
|
72
|
+
def open_tag(name, tabulation, atomic, try_one_line, class_id, attributes_hash, obj_ref, flattened)
|
73
|
+
attributes = {}
|
74
|
+
attributes.merge!(parse_object_ref(obj_ref)) if obj_ref
|
75
|
+
attributes.merge!(parse_class_and_id(class_id)) unless class_id.nil? || class_id.empty?
|
76
|
+
attributes.merge!(attributes_hash) if attributes_hash
|
77
|
+
|
78
|
+
@one_liner_pending = false
|
79
|
+
if atomic
|
80
|
+
str = " />\n"
|
81
|
+
elsif try_one_line
|
82
|
+
@one_liner_pending = true
|
83
|
+
str = ">"
|
84
|
+
elsif flattened
|
85
|
+
str = ">
"
|
86
|
+
else
|
87
|
+
str = ">\n"
|
88
|
+
end
|
89
|
+
@buffer << "#{tabs(tabulation)}<#{name}#{build_attributes(attributes)}#{str}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Creates a closing tag with the given name.
|
93
|
+
def close_tag(name, tabulation)
|
94
|
+
if @one_liner_pending
|
95
|
+
@buffer << "</#{name}>\n"
|
96
|
+
@one_liner_pending = false
|
97
|
+
else
|
98
|
+
push_text("</#{name}>", tabulation)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Opens an XHTML comment.
|
103
|
+
def open_comment(try_one_line, conditional, tabulation)
|
104
|
+
conditional << ">" if conditional
|
105
|
+
@buffer << "#{tabs(tabulation)}<!--#{conditional.to_s} "
|
106
|
+
if try_one_line
|
107
|
+
@one_liner_pending = true
|
108
|
+
else
|
109
|
+
@buffer << "\n"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Closes an XHTML comment.
|
114
|
+
def close_comment(has_conditional, tabulation)
|
115
|
+
close_tag = has_conditional ? "<![endif]-->" : "-->"
|
116
|
+
if @one_liner_pending
|
117
|
+
@buffer << " #{close_tag}\n"
|
118
|
+
@one_liner_pending = false
|
119
|
+
else
|
120
|
+
push_text(close_tag, tabulation)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Stops parsing a flat section.
|
125
|
+
def stop_flat
|
126
|
+
buffer.concat("\n")
|
127
|
+
@one_liner_pending = false
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# Gets <tt>count</tt> tabs. Mostly for internal use.
|
133
|
+
def tabs(count)
|
134
|
+
' ' * (count + @tabulation)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Iterates through the classes and ids supplied through <tt>.</tt>
|
138
|
+
# and <tt>#</tt> syntax, and returns a hash with them as attributes,
|
139
|
+
# that can then be merged with another attributes hash.
|
140
|
+
def parse_class_and_id(list)
|
141
|
+
attributes = {}
|
142
|
+
list.scan(/([#.])([-_a-zA-Z0-9]+)/) do |type, property|
|
143
|
+
case type
|
144
|
+
when '.'
|
145
|
+
if attributes[:class]
|
146
|
+
attributes[:class] += " "
|
147
|
+
else
|
148
|
+
attributes[:class] = ""
|
149
|
+
end
|
150
|
+
attributes[:class] += property
|
151
|
+
when '#'
|
152
|
+
attributes[:id] = property
|
153
|
+
end
|
154
|
+
end
|
155
|
+
attributes
|
156
|
+
end
|
157
|
+
|
158
|
+
# Takes an array of objects and uses the class and id of the first
|
159
|
+
# one to create an attributes hash.
|
160
|
+
def parse_object_ref(ref)
|
161
|
+
ref = ref[0]
|
162
|
+
# Let's make sure the value isn't nil. If it is, return the default Hash.
|
163
|
+
return {} if ref.nil?
|
164
|
+
class_name = ref.class.to_s.underscore
|
165
|
+
{:id => "#{class_name}_#{ref.id}", :class => class_name}
|
166
|
+
end
|
167
|
+
|
168
|
+
# Takes a hash and builds a list of XHTML attributes from it, returning
|
169
|
+
# the result.
|
170
|
+
def build_attributes(attributes = {})
|
171
|
+
result = attributes.collect do |a,v|
|
172
|
+
unless v.nil?
|
173
|
+
v = v.to_s
|
174
|
+
attr_wrapper = @options[:attr_wrapper]
|
175
|
+
if v.include? attr_wrapper
|
176
|
+
if v.include? @other_quote_char
|
177
|
+
v = v.gsub(attr_wrapper, @quote_escape)
|
178
|
+
else
|
179
|
+
attr_wrapper = @other_quote_char
|
180
|
+
end
|
181
|
+
end
|
182
|
+
" #{a}=#{attr_wrapper}#{v}#{attr_wrapper}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
result.sort.join
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns whether or not the given value is short enough to be rendered
|
189
|
+
# on one line.
|
190
|
+
def one_liner?(value)
|
191
|
+
value.length <= ONE_LINER_LENGTH && value.scan(/\n/).empty?
|
192
|
+
end
|
193
|
+
|
194
|
+
# Isolates the whitespace-sensitive tags in the string and uses Haml::Helpers#flatten
|
195
|
+
# to convert any endlines inside them into html entities.
|
196
|
+
def find_and_flatten(input)
|
197
|
+
input = input.to_s
|
198
|
+
input.scan(/<(textarea|code|pre)[^>]*>(.*?)<\/\1>/im) do |tag, contents|
|
199
|
+
input = input.gsub(contents, Haml::Helpers.flatten(contents))
|
200
|
+
end
|
201
|
+
input
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
class String # :nodoc
|
207
|
+
alias_method :old_comp, :<=>
|
208
|
+
def <=>(other)
|
209
|
+
if other.is_a? NilClass
|
210
|
+
-1
|
211
|
+
else
|
212
|
+
old_comp(other)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class NilClass # :nodoc:
|
218
|
+
include Comparable
|
219
|
+
|
220
|
+
def <=>(other)
|
221
|
+
other.nil? ? 0 : 1
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
data/lib/haml/engine.rb
ADDED
@@ -0,0 +1,551 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helpers'
|
2
|
+
require File.dirname(__FILE__) + '/buffer'
|
3
|
+
|
4
|
+
module Haml
|
5
|
+
# This is the class where all the parsing and processing of the HAML
|
6
|
+
# template is done. It can be directly used by the user by creating a
|
7
|
+
# new instance and calling to_html to render the template. For example:
|
8
|
+
#
|
9
|
+
# template = File.load('templates/really_cool_template.haml')
|
10
|
+
# haml_engine = Haml::Engine.new(template)
|
11
|
+
# output = haml_engine.to_html
|
12
|
+
# puts output
|
13
|
+
class Engine
|
14
|
+
# Allow access to the precompiled template
|
15
|
+
attr_reader :precompiled
|
16
|
+
|
17
|
+
# Allow reading and writing of the options hash
|
18
|
+
attr :options, true
|
19
|
+
|
20
|
+
# Designates an XHTML/XML element.
|
21
|
+
ELEMENT = '%'[0]
|
22
|
+
|
23
|
+
# Designates a <tt><div></tt> element with the given class.
|
24
|
+
DIV_CLASS = '.'[0]
|
25
|
+
|
26
|
+
# Designates a <tt><div></tt> element with the given id.
|
27
|
+
DIV_ID = '#'[0]
|
28
|
+
|
29
|
+
# Designates an XHTML/XML comment.
|
30
|
+
COMMENT = '/'[0]
|
31
|
+
|
32
|
+
# Designates an XHTML doctype.
|
33
|
+
DOCTYPE = '!'[0]
|
34
|
+
|
35
|
+
# Designates script, the result of which is output.
|
36
|
+
SCRIPT = '='[0]
|
37
|
+
|
38
|
+
# Designates script, the result of which is flattened and output.
|
39
|
+
FLAT_SCRIPT = '~'[0]
|
40
|
+
|
41
|
+
# Designates script which is run but not output.
|
42
|
+
SILENT_SCRIPT = '-'[0]
|
43
|
+
|
44
|
+
# When following SILENT_SCRIPT, designates a comment that is not output.
|
45
|
+
SILENT_COMMENT = '#'[0]
|
46
|
+
|
47
|
+
# Designates a non-parsed line.
|
48
|
+
ESCAPE = '\\'[0]
|
49
|
+
|
50
|
+
# Designates a non-parsed line. Not actually a character.
|
51
|
+
PLAIN_TEXT = -1
|
52
|
+
|
53
|
+
# Keeps track of the ASCII values of the characters that begin a
|
54
|
+
# specially-interpreted line.
|
55
|
+
SPECIAL_CHARACTERS = [
|
56
|
+
ELEMENT,
|
57
|
+
DIV_CLASS,
|
58
|
+
DIV_ID,
|
59
|
+
COMMENT,
|
60
|
+
DOCTYPE,
|
61
|
+
SCRIPT,
|
62
|
+
FLAT_SCRIPT,
|
63
|
+
SILENT_SCRIPT,
|
64
|
+
ESCAPE
|
65
|
+
]
|
66
|
+
|
67
|
+
# The value of the character that designates that a line is part
|
68
|
+
# of a multiline string.
|
69
|
+
MULTILINE_CHAR_VALUE = '|'[0]
|
70
|
+
|
71
|
+
# Characters that designate that a multiline string may be about
|
72
|
+
# to begin.
|
73
|
+
MULTILINE_STARTERS = SPECIAL_CHARACTERS - ["/"[0]]
|
74
|
+
|
75
|
+
# Keywords that appear in the middle of a Ruby block with lowered
|
76
|
+
# indentation. If a block has been started using indentation,
|
77
|
+
# lowering the indentation with one of these won't end the block.
|
78
|
+
# For example:
|
79
|
+
#
|
80
|
+
# - if foo
|
81
|
+
# %p yes!
|
82
|
+
# - else
|
83
|
+
# %p no!
|
84
|
+
#
|
85
|
+
# The block is ended after <tt>%p no!</tt>, because <tt>else</tt>
|
86
|
+
# is a member of this array.
|
87
|
+
MID_BLOCK_KEYWORDS = ['else', 'elsif', 'rescue', 'ensure', 'when']
|
88
|
+
|
89
|
+
# Creates a new instace of Haml::Engine that will compile the given
|
90
|
+
# template string when <tt>to_html</tt> is called.
|
91
|
+
# See REFERENCE for available options.
|
92
|
+
#
|
93
|
+
#--
|
94
|
+
# When adding options, remember to add information about them
|
95
|
+
# to REFERENCE!
|
96
|
+
#++
|
97
|
+
#
|
98
|
+
def initialize(template, options = {})
|
99
|
+
@options = {
|
100
|
+
:suppress_eval => false,
|
101
|
+
:attr_wrapper => "'",
|
102
|
+
:locals => {}
|
103
|
+
}.merge options
|
104
|
+
@precompiled = @options[:precompiled]
|
105
|
+
|
106
|
+
@template = template #String
|
107
|
+
@to_close_stack = []
|
108
|
+
@output_tabs = 0
|
109
|
+
@template_tabs = 0
|
110
|
+
|
111
|
+
# This is the base tabulation of the currently active
|
112
|
+
# flattened block. -1 signifies that there is no such block.
|
113
|
+
@flat_spaces = -1
|
114
|
+
|
115
|
+
# Only do the first round of pre-compiling if we really need to.
|
116
|
+
# They might be passing in the precompiled string.
|
117
|
+
do_precompile if @precompiled.nil? && (@precompiled = String.new)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Processes the template and returns the result as a string.
|
121
|
+
def to_html(scope = Object.new, &block)
|
122
|
+
@scope_object = scope
|
123
|
+
@buffer = Haml::Buffer.new(@options)
|
124
|
+
|
125
|
+
local_assigns = @options[:locals]
|
126
|
+
|
127
|
+
# Get inside the view object's world
|
128
|
+
@scope_object.instance_eval do
|
129
|
+
# Set all the local assigns
|
130
|
+
local_assigns.each do |key,val|
|
131
|
+
self.class.send(:define_method, key) { val }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Compile the @precompiled buffer
|
136
|
+
compile &block
|
137
|
+
|
138
|
+
# Return the result string
|
139
|
+
@buffer.buffer
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
#Precompile each line
|
145
|
+
def do_precompile
|
146
|
+
push_silent <<-END
|
147
|
+
def _haml_render
|
148
|
+
_hamlout = @haml_stack[-1]
|
149
|
+
_erbout = _hamlout.buffer
|
150
|
+
END
|
151
|
+
|
152
|
+
old_line = nil
|
153
|
+
old_index = nil
|
154
|
+
old_spaces = nil
|
155
|
+
old_tabs = nil
|
156
|
+
(@template + "\n\n").each_with_index do |line, index|
|
157
|
+
spaces, tabs = count_soft_tabs(line)
|
158
|
+
line = line.strip
|
159
|
+
|
160
|
+
if old_line
|
161
|
+
block_opened = tabs > old_tabs
|
162
|
+
|
163
|
+
suppress_render = handle_multiline(old_tabs, old_line, old_index)
|
164
|
+
|
165
|
+
if !suppress_render
|
166
|
+
line_empty = old_line.empty?
|
167
|
+
process_indent(old_tabs, old_line) unless line_empty
|
168
|
+
flat = @flat_spaces != -1
|
169
|
+
|
170
|
+
if flat
|
171
|
+
push_flat(old_line, old_spaces)
|
172
|
+
elsif !line_empty
|
173
|
+
process_line(old_line, old_index, block_opened)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
old_line = line
|
179
|
+
old_index = index
|
180
|
+
old_spaces = spaces
|
181
|
+
old_tabs = tabs
|
182
|
+
end
|
183
|
+
|
184
|
+
# Close all the open tags
|
185
|
+
@template_tabs.times { close }
|
186
|
+
|
187
|
+
push_silent "end"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Processes and deals with lowering indentation.
|
191
|
+
def process_indent(count, line)
|
192
|
+
if count <= @template_tabs && @template_tabs > 0
|
193
|
+
to_close = @template_tabs - count
|
194
|
+
|
195
|
+
to_close.times do |i|
|
196
|
+
offset = to_close - 1 - i
|
197
|
+
unless offset == 0 && mid_block_keyword?(line)
|
198
|
+
close
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Processes a single line of HAML.
|
205
|
+
#
|
206
|
+
# This method doesn't return anything; it simply processes the line and
|
207
|
+
# adds the appropriate code to <tt>@precompiled</tt>.
|
208
|
+
def process_line(line, index, block_opened)
|
209
|
+
case line[0]
|
210
|
+
when DIV_CLASS, DIV_ID
|
211
|
+
render_div(line, index)
|
212
|
+
when ELEMENT
|
213
|
+
render_tag(line, index)
|
214
|
+
when COMMENT
|
215
|
+
render_comment(line)
|
216
|
+
when SCRIPT
|
217
|
+
push_script(line[1..-1], false, block_opened, index)
|
218
|
+
when FLAT_SCRIPT
|
219
|
+
push_flat_script(line[1..-1], block_opened, index)
|
220
|
+
when SILENT_SCRIPT
|
221
|
+
sub_line = line[1..-1]
|
222
|
+
unless sub_line[0] == SILENT_COMMENT
|
223
|
+
push_silent(sub_line, index)
|
224
|
+
if block_opened && !mid_block_keyword?(line)
|
225
|
+
push_and_tabulate([:script])
|
226
|
+
end
|
227
|
+
end
|
228
|
+
when DOCTYPE
|
229
|
+
if line[0...3] == '!!!'
|
230
|
+
render_doctype(line)
|
231
|
+
else
|
232
|
+
push_text line
|
233
|
+
end
|
234
|
+
when ESCAPE
|
235
|
+
push_text line[1..-1]
|
236
|
+
else
|
237
|
+
push_text line
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns whether or not the line is a silent script line with one
|
242
|
+
# of Ruby's mid-block keywords.
|
243
|
+
def mid_block_keyword?(line)
|
244
|
+
line.length > 2 && line[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(line[1..-1].split[0])
|
245
|
+
end
|
246
|
+
|
247
|
+
# Deals with all the logic of figuring out whether a given line is
|
248
|
+
# the beginning, continuation, or end of a multiline sequence.
|
249
|
+
#
|
250
|
+
# This returns whether or not the line should be
|
251
|
+
# rendered normally.
|
252
|
+
def handle_multiline(count, line, index)
|
253
|
+
suppress_render = false
|
254
|
+
# Multilines are denoting by ending with a `|` (124)
|
255
|
+
if is_multiline?(line) && @multiline_buffer
|
256
|
+
# A multiline string is active, and is being continued
|
257
|
+
@multiline_buffer += line[0...-1]
|
258
|
+
suppress_render = true
|
259
|
+
elsif is_multiline?(line) && (MULTILINE_STARTERS.include? line[0])
|
260
|
+
# A multiline string has just been activated, start adding the lines
|
261
|
+
@multiline_buffer = line[0...-1]
|
262
|
+
@multiline_count = count
|
263
|
+
@multiline_index = index
|
264
|
+
process_indent(count, line)
|
265
|
+
suppress_render = true
|
266
|
+
elsif @multiline_buffer
|
267
|
+
# A multiline string has just ended, make line into the result
|
268
|
+
unless line.empty?
|
269
|
+
process_line(@multiline_buffer, @multiline_index, count > @multiline_count)
|
270
|
+
@multiline_buffer = nil
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
return suppress_render
|
275
|
+
end
|
276
|
+
|
277
|
+
# Checks whether or not +line+ is in a multiline sequence.
|
278
|
+
def is_multiline?(line) # ' '[0] == 32
|
279
|
+
line && line.length > 1 && line[-1] == MULTILINE_CHAR_VALUE && line[-2] == 32
|
280
|
+
end
|
281
|
+
|
282
|
+
# Takes <tt>@precompiled</tt>, a string buffer of Ruby code, and
|
283
|
+
# evaluates it in the context of <tt>@scope_object</tt>, after preparing
|
284
|
+
# <tt>@scope_object</tt>. The code in <tt>@precompiled</tt> populates
|
285
|
+
# <tt>@buffer</tt> with the compiled XHTML code.
|
286
|
+
def compile(&block)
|
287
|
+
# Set the local variables pointing to the buffer
|
288
|
+
buffer = @buffer
|
289
|
+
@scope_object.extend Haml::Helpers
|
290
|
+
@scope_object.instance_eval do
|
291
|
+
@haml_stack ||= Array.new
|
292
|
+
@haml_stack.push(buffer)
|
293
|
+
|
294
|
+
class << self
|
295
|
+
attr :haml_lineno # :nodoc:
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
begin
|
300
|
+
# Evaluate the buffer in the context of the scope object
|
301
|
+
@scope_object.instance_eval @precompiled
|
302
|
+
@scope_object._haml_render &block
|
303
|
+
rescue Exception => e
|
304
|
+
# Get information from the exception and format it so that
|
305
|
+
# Rails can understand it.
|
306
|
+
compile_error = e.message.scan(/\(eval\):([0-9]*):in `[-_a-zA-Z]*': compile error/)[0]
|
307
|
+
filename = "(haml)"
|
308
|
+
if @scope_object.methods.include? "haml_filename"
|
309
|
+
# For some reason that I can't figure out,
|
310
|
+
# @scope_object.methods.include? "haml_filename" && @scope_object.haml_filename
|
311
|
+
# is false when it shouldn't be. Nested if statements work, though.
|
312
|
+
|
313
|
+
if @scope_object.haml_filename
|
314
|
+
filename = "#{@scope_object.haml_filename}.haml"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
lineno = @scope_object.haml_lineno
|
318
|
+
|
319
|
+
if compile_error
|
320
|
+
eval_line = compile_error[0].to_i
|
321
|
+
line_marker = @precompiled.split("\n")[0...eval_line].grep(/@haml_lineno = [0-9]*/)[-1]
|
322
|
+
lineno = line_marker.scan(/[0-9]+/)[0].to_i if line_marker
|
323
|
+
end
|
324
|
+
|
325
|
+
e.backtrace.unshift "#{filename}:#{lineno}"
|
326
|
+
raise e
|
327
|
+
end
|
328
|
+
|
329
|
+
# Get rid of the current buffer
|
330
|
+
@scope_object.instance_eval do
|
331
|
+
@haml_stack.pop
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Evaluates <tt>text</tt> in the context of <tt>@scope_object</tt>, but
|
336
|
+
# does not output the result.
|
337
|
+
def push_silent(text, index = nil)
|
338
|
+
if index
|
339
|
+
@precompiled << "@haml_lineno = #{index + 1}\n#{text}\n"
|
340
|
+
else
|
341
|
+
# Not really DRY, but probably faster
|
342
|
+
@precompiled << "#{text}\n"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation
|
347
|
+
# without parsing it.
|
348
|
+
def push_text(text)
|
349
|
+
@precompiled << "_hamlout.push_text(#{text.dump}, #{@output_tabs})\n"
|
350
|
+
end
|
351
|
+
|
352
|
+
# Adds +text+ to <tt>@buffer</tt> while flattening text.
|
353
|
+
def push_flat(text, spaces)
|
354
|
+
tabulation = spaces - @flat_spaces
|
355
|
+
@precompiled << "_hamlout.push_text(#{text.dump}, #{tabulation > -1 ? tabulation : 0}, true)\n"
|
356
|
+
end
|
357
|
+
|
358
|
+
# Causes <tt>text</tt> to be evaluated in the context of
|
359
|
+
# <tt>@scope_object</tt> and the result to be added to <tt>@buffer</tt>.
|
360
|
+
#
|
361
|
+
# If <tt>flattened</tt> is true, Haml::Helpers#find_and_flatten is run on
|
362
|
+
# the result before it is added to <tt>@buffer</tt>
|
363
|
+
def push_script(text, flattened, block_opened, index)
|
364
|
+
unless options[:suppress_eval]
|
365
|
+
push_silent("haml_temp = #{text}", index)
|
366
|
+
out = "haml_temp = _hamlout.push_script(haml_temp, #{@output_tabs}, #{flattened})\n"
|
367
|
+
if block_opened
|
368
|
+
push_and_tabulate([:loud, out])
|
369
|
+
else
|
370
|
+
@precompiled << out
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten
|
376
|
+
# to be run on it afterwards.
|
377
|
+
def push_flat_script(text, block_opened, index)
|
378
|
+
unless text.empty?
|
379
|
+
push_script(text, true, block_opened, index)
|
380
|
+
else
|
381
|
+
start_flat(false)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
# Closes the most recent item in <tt>@to_close_stack</tt>.
|
386
|
+
def close
|
387
|
+
tag, value = @to_close_stack.pop
|
388
|
+
case tag
|
389
|
+
when :script
|
390
|
+
close_block
|
391
|
+
when :comment
|
392
|
+
close_comment value
|
393
|
+
when :element
|
394
|
+
close_tag value
|
395
|
+
when :flat
|
396
|
+
close_flat value
|
397
|
+
when :loud
|
398
|
+
close_loud value
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Puts a line in <tt>@precompiled</tt> that will add the closing tag of
|
403
|
+
# the most recently opened tag.
|
404
|
+
def close_tag(tag)
|
405
|
+
@output_tabs -= 1
|
406
|
+
@template_tabs -= 1
|
407
|
+
@precompiled << "_hamlout.close_tag(#{tag.dump}, #{@output_tabs})\n"
|
408
|
+
end
|
409
|
+
|
410
|
+
# Closes a Ruby block.
|
411
|
+
def close_block
|
412
|
+
push_silent "end"
|
413
|
+
@template_tabs -= 1
|
414
|
+
end
|
415
|
+
|
416
|
+
# Closes a comment.
|
417
|
+
def close_comment(has_conditional)
|
418
|
+
@output_tabs -= 1
|
419
|
+
@template_tabs -= 1
|
420
|
+
push_silent "_hamlout.close_comment(#{has_conditional}, #{@output_tabs})"
|
421
|
+
end
|
422
|
+
|
423
|
+
# Closes a flattened section.
|
424
|
+
def close_flat(in_tag)
|
425
|
+
@flat_spaces = -1
|
426
|
+
if in_tag
|
427
|
+
close
|
428
|
+
else
|
429
|
+
push_silent('_hamlout.stop_flat')
|
430
|
+
@template_tabs -= 1
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Closes a loud Ruby block.
|
435
|
+
def close_loud(command)
|
436
|
+
push_silent "end"
|
437
|
+
@precompiled << command
|
438
|
+
@template_tabs -= 1
|
439
|
+
end
|
440
|
+
|
441
|
+
# Parses a line that will render as an XHTML tag, and adds the code that will
|
442
|
+
# render that tag to <tt>@precompiled</tt>.
|
443
|
+
def render_tag(line, index)
|
444
|
+
line.scan(/[%]([-:_a-zA-Z0-9]+)([-_a-zA-Z0-9\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/) do |tag_name, attributes, attributes_hash, object_ref, action, value|
|
445
|
+
value = value.to_s
|
446
|
+
|
447
|
+
case action
|
448
|
+
when '/'
|
449
|
+
atomic = true
|
450
|
+
when '=', '~'
|
451
|
+
parse = true
|
452
|
+
else
|
453
|
+
value = value.strip
|
454
|
+
end
|
455
|
+
|
456
|
+
flattened = (action == '~')
|
457
|
+
value_exists = !value.empty?
|
458
|
+
attributes_hash = "nil" unless attributes_hash
|
459
|
+
object_ref = "nil" unless object_ref
|
460
|
+
|
461
|
+
push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{@output_tabs}, #{atomic.inspect}, #{value_exists.inspect}, #{attributes.inspect}, #{attributes_hash}, #{object_ref}, #{flattened.inspect})"
|
462
|
+
|
463
|
+
unless atomic
|
464
|
+
push_and_tabulate([:element, tag_name])
|
465
|
+
@output_tabs += 1
|
466
|
+
|
467
|
+
if value_exists
|
468
|
+
if parse
|
469
|
+
push_script(value, flattened, false, index)
|
470
|
+
else
|
471
|
+
push_text(value)
|
472
|
+
end
|
473
|
+
close
|
474
|
+
elsif flattened
|
475
|
+
start_flat(true)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# Renders a line that creates an XHTML tag and has an implicit div because of
|
482
|
+
# <tt>.</tt> or <tt>#</tt>.
|
483
|
+
def render_div(line, index)
|
484
|
+
render_tag('%div' + line, index)
|
485
|
+
end
|
486
|
+
|
487
|
+
# Renders an XHTML comment.
|
488
|
+
def render_comment(line)
|
489
|
+
conditional, content = line.scan(/\/(\[[a-zA-Z0-9 \.]*\])?(.*)/)[0]
|
490
|
+
content = content.strip
|
491
|
+
try_one_line = !content.empty?
|
492
|
+
push_silent "_hamlout.open_comment(#{try_one_line}, #{conditional.inspect}, #{@output_tabs})"
|
493
|
+
@output_tabs += 1
|
494
|
+
push_and_tabulate([:comment, !conditional.nil?])
|
495
|
+
if try_one_line
|
496
|
+
push_text content
|
497
|
+
close
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
# Renders an XHTML doctype or XML shebang.
|
502
|
+
def render_doctype(line)
|
503
|
+
line = line[3..-1].lstrip.downcase
|
504
|
+
if line[0...3] == "xml"
|
505
|
+
encoding = line.split[1] || "utf-8"
|
506
|
+
wrapper = @options[:attr_wrapper]
|
507
|
+
doctype = "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{encoding}#{wrapper} ?>"
|
508
|
+
else
|
509
|
+
version, type = line.scan(/([0-9]\.[0-9])?[\s]*([a-zA-Z]*)/)[0]
|
510
|
+
if version == "1.1"
|
511
|
+
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
|
512
|
+
else
|
513
|
+
case type
|
514
|
+
when "strict"
|
515
|
+
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
|
516
|
+
when "frameset"
|
517
|
+
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
|
518
|
+
else
|
519
|
+
doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
push_text doctype
|
524
|
+
end
|
525
|
+
|
526
|
+
# Starts a flattened block.
|
527
|
+
def start_flat(in_tag)
|
528
|
+
# @flat_spaces is the number of indentations in the template
|
529
|
+
# that forms the base of the flattened area
|
530
|
+
if in_tag
|
531
|
+
@to_close_stack.push([:flat, true])
|
532
|
+
else
|
533
|
+
push_and_tabulate([:flat])
|
534
|
+
end
|
535
|
+
@flat_spaces = @template_tabs * 2
|
536
|
+
end
|
537
|
+
|
538
|
+
# Counts the tabulation of a line.
|
539
|
+
def count_soft_tabs(line)
|
540
|
+
spaces = line.index(/[^ ]/)
|
541
|
+
spaces ? [spaces, spaces/2] : []
|
542
|
+
end
|
543
|
+
|
544
|
+
# Pushes value onto <tt>@to_close_stack</tt> and increases
|
545
|
+
# <tt>@template_tabs</tt>.
|
546
|
+
def push_and_tabulate(value)
|
547
|
+
@to_close_stack.push(value)
|
548
|
+
@template_tabs += 1
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|