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.

Files changed (48) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/REFERENCE +662 -0
  3. data/Rakefile +167 -0
  4. data/VERSION +1 -0
  5. data/bin/haml +18 -0
  6. data/lib/haml/buffer.rb +224 -0
  7. data/lib/haml/engine.rb +551 -0
  8. data/lib/haml/helpers.rb +220 -0
  9. data/lib/haml/helpers/action_view_mods.rb +53 -0
  10. data/lib/haml/template.rb +138 -0
  11. data/test/benchmark.rb +62 -0
  12. data/test/engine_test.rb +93 -0
  13. data/test/helper_test.rb +105 -0
  14. data/test/mocks/article.rb +6 -0
  15. data/test/profile.rb +45 -0
  16. data/test/results/content_for_layout.xhtml +16 -0
  17. data/test/results/eval_suppressed.xhtml +2 -0
  18. data/test/results/helpers.xhtml +50 -0
  19. data/test/results/helpful.xhtml +5 -0
  20. data/test/results/just_stuff.xhtml +36 -0
  21. data/test/results/list.xhtml +12 -0
  22. data/test/results/original_engine.xhtml +24 -0
  23. data/test/results/partials.xhtml +20 -0
  24. data/test/results/silent_script.xhtml +74 -0
  25. data/test/results/standard.xhtml +42 -0
  26. data/test/results/tag_parsing.xhtml +28 -0
  27. data/test/results/very_basic.xhtml +7 -0
  28. data/test/results/whitespace_handling.xhtml +51 -0
  29. data/test/rhtml/standard.rhtml +51 -0
  30. data/test/runner.rb +15 -0
  31. data/test/template_test.rb +137 -0
  32. data/test/templates/_partial.haml +7 -0
  33. data/test/templates/_text_area.haml +3 -0
  34. data/test/templates/content_for_layout.haml +10 -0
  35. data/test/templates/eval_suppressed.haml +5 -0
  36. data/test/templates/helpers.haml +39 -0
  37. data/test/templates/helpful.haml +6 -0
  38. data/test/templates/just_stuff.haml +29 -0
  39. data/test/templates/list.haml +12 -0
  40. data/test/templates/original_engine.haml +17 -0
  41. data/test/templates/partialize.haml +1 -0
  42. data/test/templates/partials.haml +12 -0
  43. data/test/templates/silent_script.haml +40 -0
  44. data/test/templates/standard.haml +40 -0
  45. data/test/templates/tag_parsing.haml +24 -0
  46. data/test/templates/very_basic.haml +4 -0
  47. data/test/templates/whitespace_handling.haml +66 -0
  48. metadata +108 -0
@@ -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
@@ -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
@@ -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] == '"' ? "&quot;" : "&apos;"
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 = ">&#x000A;"
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
+
@@ -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