Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 193
- Log:
First stage commit of Typo 4.1, modified for the ROOL site.
Includes all local modifications but a final pass needs to be
made to delete any files left over from earlier Typo versions
that shouldn't be here anymore. See the 'tags' section of the
repository for a clean Typo 4.1 tree.Note that symlinks to shared files in the RISC OS Open theme
directory have been deliberately included this time around; I
decided that on balance it was better to leave them in as
placeholders, since unlike symlinks in app/views/shared, the
Typo theme structure is not a standard Rails concept.
- Author:
- rool
- Date:
- Wed Apr 04 18:51:02 +0100 2007
- Size:
- 17469 Bytes
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.strip #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-#").each_with_index do |line, index| |
157 | spaces, tabs = count_soft_tabs(line) |
158 | line = line.strip |
159 | |
160 | if !line.empty? |
161 | if old_line |
162 | block_opened = tabs > old_tabs && !line.empty? |
163 | |
164 | suppress_render = handle_multiline(old_tabs, old_line, old_index) |
165 | |
166 | if !suppress_render |
167 | line_empty = old_line.empty? |
168 | process_indent(old_tabs, old_line) unless line_empty |
169 | flat = @flat_spaces != -1 |
170 | |
171 | if flat |
172 | push_flat(old_line, old_spaces) |
173 | elsif !line_empty |
174 | process_line(old_line, old_index, block_opened) |
175 | end |
176 | end |
177 | end |
178 | |
179 | old_line = line |
180 | old_index = index |
181 | old_spaces = spaces |
182 | old_tabs = tabs |
183 | elsif @flat_spaces != -1 |
184 | push_flat(old_line, old_spaces) |
185 | old_line = '' |
186 | old_spaces = 0 |
187 | end |
188 | end |
189 | |
190 | # Close all the open tags |
191 | @template_tabs.times { close } |
192 | |
193 | push_silent "end" |
194 | end |
195 | |
196 | # Processes and deals with lowering indentation. |
197 | def process_indent(count, line) |
198 | if count <= @template_tabs && @template_tabs > 0 |
199 | to_close = @template_tabs - count |
200 | |
201 | to_close.times do |i| |
202 | offset = to_close - 1 - i |
203 | unless offset == 0 && mid_block_keyword?(line) |
204 | close |
205 | end |
206 | end |
207 | end |
208 | end |
209 | |
210 | # Processes a single line of HAML. |
211 | # |
212 | # This method doesn't return anything; it simply processes the line and |
213 | # adds the appropriate code to <tt>@precompiled</tt>. |
214 | def process_line(line, index, block_opened) |
215 | case line[0] |
216 | when DIV_CLASS, DIV_ID |
217 | render_div(line, index) |
218 | when ELEMENT |
219 | render_tag(line, index) |
220 | when COMMENT |
221 | render_comment(line) |
222 | when SCRIPT |
223 | push_script(line[1..-1], false, block_opened, index) |
224 | when FLAT_SCRIPT |
225 | push_flat_script(line[1..-1], block_opened, index) |
226 | when SILENT_SCRIPT |
227 | sub_line = line[1..-1] |
228 | unless sub_line[0] == SILENT_COMMENT |
229 | push_silent(sub_line, index) |
230 | if block_opened && !mid_block_keyword?(line) |
231 | push_and_tabulate([:script]) |
232 | end |
233 | end |
234 | when DOCTYPE |
235 | if line[0...3] == '!!!' |
236 | render_doctype(line) |
237 | else |
238 | push_text line |
239 | end |
240 | when ESCAPE |
241 | push_text line[1..-1] |
242 | else |
243 | push_text line |
244 | end |
245 | end |
246 | |
247 | # Returns whether or not the line is a silent script line with one |
248 | # of Ruby's mid-block keywords. |
249 | def mid_block_keyword?(line) |
250 | line.length > 2 && line[0] == SILENT_SCRIPT && MID_BLOCK_KEYWORDS.include?(line[1..-1].split[0]) |
251 | end |
252 | |
253 | # Deals with all the logic of figuring out whether a given line is |
254 | # the beginning, continuation, or end of a multiline sequence. |
255 | # |
256 | # This returns whether or not the line should be |
257 | # rendered normally. |
258 | def handle_multiline(count, line, index) |
259 | suppress_render = false |
260 | # Multilines are denoting by ending with a `|` (124) |
261 | if is_multiline?(line) && @multiline_buffer |
262 | # A multiline string is active, and is being continued |
263 | @multiline_buffer += line[0...-1] |
264 | suppress_render = true |
265 | elsif is_multiline?(line) && (MULTILINE_STARTERS.include? line[0]) |
266 | # A multiline string has just been activated, start adding the lines |
267 | @multiline_buffer = line[0...-1] |
268 | @multiline_count = count |
269 | @multiline_index = index |
270 | process_indent(count, line) |
271 | suppress_render = true |
272 | elsif @multiline_buffer |
273 | # A multiline string has just ended, make line into the result |
274 | unless line.empty? |
275 | process_line(@multiline_buffer, @multiline_index, count > @multiline_count) |
276 | @multiline_buffer = nil |
277 | end |
278 | end |
279 | |
280 | return suppress_render |
281 | end |
282 | |
283 | # Checks whether or not +line+ is in a multiline sequence. |
284 | def is_multiline?(line) # ' '[0] == 32 |
285 | line && line.length > 1 && line[-1] == MULTILINE_CHAR_VALUE && line[-2] == 32 |
286 | end |
287 | |
288 | # Takes <tt>@precompiled</tt>, a string buffer of Ruby code, and |
289 | # evaluates it in the context of <tt>@scope_object</tt>, after preparing |
290 | # <tt>@scope_object</tt>. The code in <tt>@precompiled</tt> populates |
291 | # <tt>@buffer</tt> with the compiled XHTML code. |
292 | def compile(&block) |
293 | # Set the local variables pointing to the buffer |
294 | buffer = @buffer |
295 | @scope_object.extend Haml::Helpers |
296 | @scope_object.instance_eval do |
297 | @haml_stack ||= Array.new |
298 | @haml_stack.push(buffer) |
299 | |
300 | class << self |
301 | attr :haml_lineno # :nodoc: |
302 | end |
303 | end |
304 | |
305 | begin |
306 | # Evaluate the buffer in the context of the scope object |
307 | @scope_object.instance_eval @precompiled |
308 | @scope_object._haml_render &block |
309 | rescue Exception => e |
310 | # Get information from the exception and format it so that |
311 | # Rails can understand it. |
312 | compile_error = e.message.scan(/\(eval\):([0-9]*):in `[-_a-zA-Z]*': compile error/)[0] |
313 | filename = "(haml)" |
314 | if @scope_object.methods.include? "haml_filename" |
315 | # For some reason that I can't figure out, |
316 | # @scope_object.methods.include? "haml_filename" && @scope_object.haml_filename |
317 | # is false when it shouldn't be. Nested if statements work, though. |
318 | |
319 | if @scope_object.haml_filename |
320 | filename = "#{@scope_object.haml_filename}.haml" |
321 | end |
322 | end |
323 | lineno = @scope_object.haml_lineno |
324 | |
325 | if compile_error |
326 | eval_line = compile_error[0].to_i |
327 | line_marker = @precompiled.split("\n")[0...eval_line].grep(/@haml_lineno = [0-9]*/)[-1] |
328 | lineno = line_marker.scan(/[0-9]+/)[0].to_i if line_marker |
329 | end |
330 | |
331 | e.backtrace.unshift "#{filename}:#{lineno}" |
332 | raise e |
333 | end |
334 | |
335 | # Get rid of the current buffer |
336 | @scope_object.instance_eval do |
337 | @haml_stack.pop |
338 | end |
339 | end |
340 | |
341 | # Evaluates <tt>text</tt> in the context of <tt>@scope_object</tt>, but |
342 | # does not output the result. |
343 | def push_silent(text, index = nil) |
344 | if index |
345 | @precompiled << "@haml_lineno = #{index + 1}\n#{text}\n" |
346 | else |
347 | # Not really DRY, but probably faster |
348 | @precompiled << "#{text}\n" |
349 | end |
350 | end |
351 | |
352 | # Adds <tt>text</tt> to <tt>@buffer</tt> with appropriate tabulation |
353 | # without parsing it. |
354 | def push_text(text) |
355 | @precompiled << "_hamlout.push_text(#{text.dump}, #{@output_tabs})\n" |
356 | end |
357 | |
358 | # Adds +text+ to <tt>@buffer</tt> while flattening text. |
359 | def push_flat(text, spaces) |
360 | tabulation = spaces - @flat_spaces |
361 | @precompiled << "_hamlout.push_text(#{text.dump}, #{tabulation > -1 ? tabulation : 0}, true)\n" |
362 | end |
363 | |
364 | # Causes <tt>text</tt> to be evaluated in the context of |
365 | # <tt>@scope_object</tt> and the result to be added to <tt>@buffer</tt>. |
366 | # |
367 | # If <tt>flattened</tt> is true, Haml::Helpers#find_and_flatten is run on |
368 | # the result before it is added to <tt>@buffer</tt> |
369 | def push_script(text, flattened, block_opened, index) |
370 | unless options[:suppress_eval] |
371 | push_silent("haml_temp = #{text}", index) |
372 | out = "haml_temp = _hamlout.push_script(haml_temp, #{@output_tabs}, #{flattened})\n" |
373 | if block_opened |
374 | push_and_tabulate([:loud, out]) |
375 | else |
376 | @precompiled << out |
377 | end |
378 | end |
379 | end |
380 | |
381 | # Causes <tt>text</tt> to be evaluated, and Haml::Helpers#find_and_flatten |
382 | # to be run on it afterwards. |
383 | def push_flat_script(text, block_opened, index) |
384 | unless text.empty? |
385 | push_script(text, true, block_opened, index) |
386 | else |
387 | start_flat(false) |
388 | end |
389 | end |
390 | |
391 | # Closes the most recent item in <tt>@to_close_stack</tt>. |
392 | def close |
393 | tag, value = @to_close_stack.pop |
394 | case tag |
395 | when :script |
396 | close_block |
397 | when :comment |
398 | close_comment value |
399 | when :element |
400 | close_tag value |
401 | when :flat |
402 | close_flat value |
403 | when :loud |
404 | close_loud value |
405 | end |
406 | end |
407 | |
408 | # Puts a line in <tt>@precompiled</tt> that will add the closing tag of |
409 | # the most recently opened tag. |
410 | def close_tag(tag) |
411 | @output_tabs -= 1 |
412 | @template_tabs -= 1 |
413 | @precompiled << "_hamlout.close_tag(#{tag.dump}, #{@output_tabs})\n" |
414 | end |
415 | |
416 | # Closes a Ruby block. |
417 | def close_block |
418 | push_silent "end" |
419 | @template_tabs -= 1 |
420 | end |
421 | |
422 | # Closes a comment. |
423 | def close_comment(has_conditional) |
424 | @output_tabs -= 1 |
425 | @template_tabs -= 1 |
426 | push_silent "_hamlout.close_comment(#{has_conditional}, #{@output_tabs})" |
427 | end |
428 | |
429 | # Closes a flattened section. |
430 | def close_flat(in_tag) |
431 | @flat_spaces = -1 |
432 | if in_tag |
433 | close |
434 | else |
435 | push_silent('_hamlout.stop_flat') |
436 | @template_tabs -= 1 |
437 | end |
438 | end |
439 | |
440 | # Closes a loud Ruby block. |
441 | def close_loud(command) |
442 | push_silent "end" |
443 | @precompiled << command |
444 | @template_tabs -= 1 |
445 | end |
446 | |
447 | # Parses a line that will render as an XHTML tag, and adds the code that will |
448 | # render that tag to <tt>@precompiled</tt>. |
449 | def render_tag(line, index) |
450 | line.scan(/[%]([-:_a-zA-Z0-9]+)([-_a-zA-Z0-9\.\#]*)(\{.*\})?(\[.*\])?([=\/\~]?)?(.*)?/) do |tag_name, attributes, attributes_hash, object_ref, action, value| |
451 | value = value.to_s |
452 | |
453 | case action |
454 | when '/' |
455 | atomic = true |
456 | when '=', '~' |
457 | parse = true |
458 | else |
459 | value = value.strip |
460 | end |
461 | |
462 | flattened = (action == '~') |
463 | value_exists = !value.empty? |
464 | attributes_hash = "nil" unless attributes_hash |
465 | object_ref = "nil" unless object_ref |
466 | |
467 | push_silent "_hamlout.open_tag(#{tag_name.inspect}, #{@output_tabs}, #{atomic.inspect}, #{value_exists.inspect}, #{attributes.inspect}, #{attributes_hash}, #{object_ref}, #{flattened.inspect})" |
468 | |
469 | unless atomic |
470 | push_and_tabulate([:element, tag_name]) |
471 | @output_tabs += 1 |
472 | |
473 | if value_exists |
474 | if parse |
475 | push_script(value, flattened, false, index) |
476 | else |
477 | push_text(value) |
478 | end |
479 | close |
480 | elsif flattened |
481 | start_flat(true) |
482 | end |
483 | end |
484 | end |
485 | end |
486 | |
487 | # Renders a line that creates an XHTML tag and has an implicit div because of |
488 | # <tt>.</tt> or <tt>#</tt>. |
489 | def render_div(line, index) |
490 | render_tag('%div' + line, index) |
491 | end |
492 | |
493 | # Renders an XHTML comment. |
494 | def render_comment(line) |
495 | conditional, content = line.scan(/\/(\[[a-zA-Z0-9 \.]*\])?(.*)/)[0] |
496 | content = content.strip |
497 | try_one_line = !content.empty? |
498 | push_silent "_hamlout.open_comment(#{try_one_line}, #{conditional.inspect}, #{@output_tabs})" |
499 | @output_tabs += 1 |
500 | push_and_tabulate([:comment, !conditional.nil?]) |
501 | if try_one_line |
502 | push_text content |
503 | close |
504 | end |
505 | end |
506 | |
507 | # Renders an XHTML doctype or XML shebang. |
508 | def render_doctype(line) |
509 | line = line[3..-1].lstrip.downcase |
510 | if line[0...3] == "xml" |
511 | encoding = line.split[1] || "utf-8" |
512 | wrapper = @options[:attr_wrapper] |
513 | doctype = "<?xml version=#{wrapper}1.0#{wrapper} encoding=#{wrapper}#{encoding}#{wrapper} ?>" |
514 | else |
515 | version, type = line.scan(/([0-9]\.[0-9])?[\s]*([a-zA-Z]*)/)[0] |
516 | if version == "1.1" |
517 | doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">' |
518 | else |
519 | case type |
520 | when "strict" |
521 | doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' |
522 | when "frameset" |
523 | doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">' |
524 | else |
525 | doctype = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">' |
526 | end |
527 | end |
528 | end |
529 | push_text doctype |
530 | end |
531 | |
532 | # Starts a flattened block. |
533 | def start_flat(in_tag) |
534 | # @flat_spaces is the number of indentations in the template |
535 | # that forms the base of the flattened area |
536 | if in_tag |
537 | @to_close_stack.push([:flat, true]) |
538 | else |
539 | push_and_tabulate([:flat]) |
540 | end |
541 | @flat_spaces = @template_tabs * 2 |
542 | end |
543 | |
544 | # Counts the tabulation of a line. |
545 | def count_soft_tabs(line) |
546 | spaces = line.index(/[^ ]/) |
547 | spaces ? [spaces, spaces/2] : [] |
548 | end |
549 | |
550 | # Pushes value onto <tt>@to_close_stack</tt> and increases |
551 | # <tt>@template_tabs</tt>. |
552 | def push_and_tabulate(value) |
553 | @to_close_stack.push(value) |
554 | @template_tabs += 1 |
555 | end |
556 | end |
557 | end |