htree 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data.tar.gz.sig +4 -0
  2. data/Makefile +20 -0
  3. data/Manifest +58 -0
  4. data/README +61 -0
  5. data/Rakefile +37 -0
  6. data/htree.gemspec +32 -0
  7. data/init.rb +1 -0
  8. data/install.rb +112 -0
  9. data/lib/htree.rb +97 -0
  10. data/lib/htree/container.rb +8 -0
  11. data/lib/htree/context.rb +69 -0
  12. data/lib/htree/display.rb +46 -0
  13. data/lib/htree/doc.rb +149 -0
  14. data/lib/htree/elem.rb +262 -0
  15. data/lib/htree/encoder.rb +217 -0
  16. data/lib/htree/equality.rb +219 -0
  17. data/lib/htree/extract_text.rb +37 -0
  18. data/lib/htree/fstr.rb +32 -0
  19. data/lib/htree/gencode.rb +193 -0
  20. data/lib/htree/htmlinfo.rb +672 -0
  21. data/lib/htree/inspect.rb +108 -0
  22. data/lib/htree/leaf.rb +92 -0
  23. data/lib/htree/loc.rb +369 -0
  24. data/lib/htree/modules.rb +49 -0
  25. data/lib/htree/name.rb +122 -0
  26. data/lib/htree/output.rb +212 -0
  27. data/lib/htree/parse.rb +410 -0
  28. data/lib/htree/raw_string.rb +127 -0
  29. data/lib/htree/regexp-util.rb +19 -0
  30. data/lib/htree/rexml.rb +131 -0
  31. data/lib/htree/scan.rb +176 -0
  32. data/lib/htree/tag.rb +113 -0
  33. data/lib/htree/template.rb +961 -0
  34. data/lib/htree/text.rb +115 -0
  35. data/lib/htree/traverse.rb +497 -0
  36. data/test-all.rb +5 -0
  37. data/test/assign.html +1 -0
  38. data/test/template.html +4 -0
  39. data/test/test-attr.rb +67 -0
  40. data/test/test-charset.rb +79 -0
  41. data/test/test-context.rb +29 -0
  42. data/test/test-display_xml.rb +45 -0
  43. data/test/test-elem-new.rb +101 -0
  44. data/test/test-encoder.rb +53 -0
  45. data/test/test-equality.rb +55 -0
  46. data/test/test-extract_text.rb +18 -0
  47. data/test/test-gencode.rb +27 -0
  48. data/test/test-leaf.rb +25 -0
  49. data/test/test-loc.rb +60 -0
  50. data/test/test-namespace.rb +147 -0
  51. data/test/test-output.rb +133 -0
  52. data/test/test-parse.rb +115 -0
  53. data/test/test-raw_string.rb +17 -0
  54. data/test/test-rexml.rb +70 -0
  55. data/test/test-scan.rb +153 -0
  56. data/test/test-security.rb +37 -0
  57. data/test/test-subnode.rb +142 -0
  58. data/test/test-template.rb +313 -0
  59. data/test/test-text.rb +43 -0
  60. data/test/test-traverse.rb +69 -0
  61. metadata +166 -0
  62. metadata.gz.sig +1 -0
@@ -0,0 +1,113 @@
1
+ require 'htree/raw_string'
2
+ require 'htree/text'
3
+ require 'htree/scan' # for Pat::Name and Pat::Nmtoken
4
+ require 'htree/context'
5
+ require 'htree/name'
6
+ require 'htree/fstr'
7
+
8
+ module HTree
9
+ # :stopdoc:
10
+
11
+ class STag
12
+ def initialize(name, attributes=[], inherited_context=DefaultContext)
13
+ init_raw_string
14
+ # normalize xml declaration name and attribute value.
15
+ attributes = attributes.map {|aname, val|
16
+ if !(Name === aname) && /\A(?:#{Pat::Name}?\{.*\})?#{Pat::Nmtoken}\z/o !~ aname
17
+ raise HTree::Error, "invalid attribute name: #{aname.inspect}"
18
+ end
19
+ if !(Name === aname) && /\Axmlns(?:\z|:)/ =~ aname
20
+ aname = Name.parse_attribute_name(aname, nil)
21
+ end
22
+ val = val.to_node if HTree::Location === val
23
+ val = Text.new(val) unless Text === val
24
+ [aname, val]
25
+ }
26
+
27
+ @inherited_context = inherited_context
28
+ @xmlns_decls = {}
29
+
30
+ # validate namespace consistency of given Name objects.
31
+ if Name === name
32
+ @xmlns_decls[name.namespace_prefix] = name.namespace_uri
33
+ end
34
+ attributes.each {|aname, text|
35
+ next unless Name === aname
36
+ next if aname.xmlns?
37
+ if aname.namespace_prefix && aname.namespace_uri
38
+ if @xmlns_decls.include? aname.namespace_prefix
39
+ if @xmlns_decls[aname.namespace_prefix] != aname.namespace_uri
40
+ raise ArgumentError, "inconsistent namespace use: #{aname.namespace_prefix} is used as #{@xmlns_decls[aname.namespace_prefix]} and #{aname.namespace_uri}"
41
+ end
42
+ else
43
+ @xmlns_decls[aname.namespace_prefix] = aname.namespace_uri
44
+ end
45
+ end
46
+ }
47
+
48
+ attributes.each {|aname, text|
49
+ next unless Name === aname
50
+ next unless aname.xmlns?
51
+ next if @xmlns_decls.include? aname.local_name
52
+ if aname.local_name
53
+ @xmlns_decls[aname.local_name] = text.to_s
54
+ else
55
+ uri = text.to_s
56
+ @xmlns_decls[nil] = uri
57
+ end
58
+ }
59
+
60
+ @context = make_context(@inherited_context)
61
+
62
+ if Name === name
63
+ @name = name
64
+ else
65
+ @name = Name.parse_element_name(name, @context)
66
+ end
67
+
68
+ @attributes = attributes.map {|aname, text|
69
+ aname = Name.parse_attribute_name(aname, @context) unless Name === aname
70
+ if !aname.namespace_prefix && !aname.namespace_uri.empty?
71
+ # xxx: should recover error?
72
+ raise HTree::Error, "global attribute without namespace prefix: #{aname.inspect}"
73
+ end
74
+ [aname, text]
75
+ }
76
+ @attributes.freeze
77
+ end
78
+ attr_reader :attributes, :inherited_context, :context
79
+
80
+ def element_name
81
+ @name
82
+ end
83
+
84
+ def make_context(inherited_context)
85
+ inherited_context.subst_namespaces(@xmlns_decls)
86
+ end
87
+
88
+ def each_namespace_attribute
89
+ @xmlns_decls.each {|name, uri|
90
+ yield name, uri
91
+ }
92
+ nil
93
+ end
94
+
95
+ def each_attribute
96
+ @attributes.each {|name, text|
97
+ next if name.xmlns?
98
+ yield name, text
99
+ }
100
+ nil
101
+ end
102
+ end
103
+
104
+ class ETag
105
+ def initialize(qualified_name)
106
+ init_raw_string
107
+ @qualified_name = HTree.frozen_string(qualified_name)
108
+ end
109
+ attr_reader :qualified_name
110
+ end
111
+
112
+ # :startdoc:
113
+ end
@@ -0,0 +1,961 @@
1
+ # = Template Engine
2
+ #
3
+ # The htree template engine converts HTML and some data to HTML or XML.
4
+ #
5
+ # == Template Method Summary
6
+ #
7
+ # - HTree.expand_template(<i>template_pathname</i>) -> $stdout
8
+ # - HTree.expand_template(<i>template_pathname</i>, <i>obj</i>) -> $stdout
9
+ # - HTree.expand_template(<i>template_pathname</i>, <i>obj</i>, <i>out</i>) -> <i>out</i>
10
+ # - HTree.expand_template(<i>template_pathname</i>, <i>obj</i>, <i>out</i>, <i>encoding</i>) -> <i>out</i>
11
+ #
12
+ # - HTree.expand_template{<i>template_string</i>} -> $stdout
13
+ # - HTree.expand_template(<i>out</i>) {<i>template_string</i>} -> <i>out</i>
14
+ # - HTree.expand_template(<i>out</i>, <i>encoding</i>) {<i>template_string</i>} -> <i>out</i>
15
+ #
16
+ # - HTree.compile_template(<i>template_string</i>) -> Module
17
+ # - HTree{<i>template_string</i>} -> HTree::Doc
18
+ #
19
+ # Note that the following method, HTree(), is not a template method.
20
+ #
21
+ # - HTree(<i>html_string</i>) -> HTree::Doc
22
+ #
23
+ # == Template Directives.
24
+ #
25
+ # A template directive is described as a special HTML attribute which name
26
+ # begins with underscore.
27
+ #
28
+ # The template directives are listed as follows.
29
+ #
30
+ # - <elem \_attr_<i>name</i>="<i>expr</i>">content</elem>
31
+ # - <elem _text="<i>expr</i>">dummy-content</elem>
32
+ # - <elem _text><i>expr</i></elem>
33
+ # - <elem _tree="<i>expr</i>">dummy-content</elem>
34
+ # - <elem _tree><i>expr</i></elem>
35
+ # - <elem _if="<i>expr</i>" _else="<i>mod.name(args)</i>">then-content</elem>
36
+ # - <elem _iter="<i>expr.meth(args)//vars</i>">content</elem>
37
+ # - <elem _iter_content="<i>expr.meth(args)//vars</i>">content</elem>
38
+ # - <elem _call="<i>mod.name(args)</i>">dummy-content</elem>
39
+ # - <elem _template="<i>name(vars)</i>">body</elem>
40
+ #
41
+ # === Template Semantics
42
+ #
43
+ # - attribute substitution
44
+ # - <elem \_attr_<i>name</i>="<i>expr</i>">content</elem>
45
+ #
46
+ # \_attr_<i>name</i> is used for a dynamic attribute.
47
+ #
48
+ # <elem _attr_xxx="..."/>
49
+ # -> <elem xxx="..."/>
50
+ #
51
+ # It is expanded to <i>name</i>="content".
52
+ # The content is generated by evaluating _expr_.
53
+ # Usually you don't need to care escaping: &, <, > and " are automatically escaped.
54
+ # If you need to output character references,
55
+ # the value of _expr_ should be an object which have a +rcdata+ method such as an HTree::Text.
56
+ # If the value has a +rcdata+ method,
57
+ # it is called and the result is used as the content with escaping <, > and ".
58
+ #
59
+ # \_attr_<i>name</i> can be used multiple times in single element.
60
+ #
61
+ # - text substitution
62
+ # - <elem _text="<i>expr</i>">dummy-content</elem>
63
+ # - <elem _text><i>expr</i></elem>
64
+ #
65
+ # _text substitutes the content of the element by the string
66
+ # evaluated from _expr_.
67
+ # _expr_ is described in the attribute value or the content of the element.
68
+ #
69
+ # If a result of _expr_ have &, < and/or >, they are automatically escaped.
70
+ # If you need to output character references,
71
+ # the value of _expr_ should be an object which have a +rcdata+ method such as an HTree::Text.
72
+ # If the value has a +rcdata+ method,
73
+ # it is called and the result is used as the content with escaping < and >.
74
+ #
75
+ # If the element is span or div, and there is no other attributes,
76
+ # no tags are produced.
77
+ #
78
+ # <elem _text="...">dummy-content</elem>
79
+ # -> <elem>...</elem>
80
+ #
81
+ # - tree substitution
82
+ # - <elem _tree="<i>expr</i>">dummy-content</elem>
83
+ # - <elem _tree><i>expr</i></elem>
84
+ #
85
+ # _tree substitutes the content of the element by the htree object
86
+ # evaluated from _expr_.
87
+ # _expr_ is described in the attribute value or the content of the element.
88
+ #
89
+ # If the element is span or div, and there is no other attributes,
90
+ # no tags are produced.
91
+ #
92
+ # <elem _tree="...">dummy-content</elem>
93
+ # -> <elem>...</elem>
94
+ #
95
+ # - conditional
96
+ # - <elem _if="<i>expr</i>">then-content</elem>
97
+ # - <elem _if="<i>expr</i>" _else="<i>name(args)</i>">then-content</elem>
98
+ #
99
+ # _if is used for conditional.
100
+ #
101
+ # If <i>expr</i> is evaluated to true, it expands as follows
102
+ # regardless of existence of _else.
103
+ #
104
+ # <elem _if="<i>expr</i>">then-content</elem>
105
+ # -> <elem>then-content</elem>
106
+ #
107
+ # If <i>expr</i> is evaluated to false, it expands using _else.
108
+ # If _else is not given, it expands to empty.
109
+ # If _else is given, it expands as follows.
110
+ #
111
+ # <elem _if="<i>expr</i>" _else="<i>name(args)</i>">then-content</elem>
112
+ # -> <elem _call="<i>name(args)</i>">then-content</elem>
113
+ # -> see _call for further expansion.
114
+ #
115
+ # It is expanded to <elem>then-content</elem> if _expr_ is evaluated to
116
+ # a true value.
117
+ # Otherwise, it is replaced by other template specified by _else attribute.
118
+ # If _else attribute is not given, it just replaced by empty.
119
+ #
120
+ # - iteration
121
+ # - <elem _iter="<i>expr.meth(args)//vars</i>">content</elem>
122
+ # - <elem _iter_content="<i>expr.meth(args)//vars</i>">content</elem>
123
+ #
124
+ # _iter and _iter_content is used for iteration.
125
+ # _iter iterates the element itself but _iter_content iterates the content.
126
+ #
127
+ # <outer _iter="..."><inner/></outer>
128
+ # -> <outer><inner/></outer><outer><inner/></outer>...
129
+ #
130
+ # <outer _iter_content="..."><inner/></outer>
131
+ # -> <outer><inner/><inner/>...</outer>
132
+ #
133
+ # <i>expr.meth(args)</i> specifies iterator method call.
134
+ # It is actually called with a block.
135
+ # The block have block parameters <i>vars</i>.
136
+ # <i>vars</i> must be variables separated by comma.
137
+ #
138
+ # - template call
139
+ # - <elem _call="<i>name(args)</i>">dummy-content</elem>
140
+ # - <elem _call="<i>mod.name(args)</i>">dummy-content</elem>
141
+ #
142
+ # _call is used to expand a template function.
143
+ # The template function is defined by _template.
144
+ #
145
+ # <d _template="m">...</d>
146
+ # <c _call="m">...</c>
147
+ # -> <d>...</d>
148
+ #
149
+ # A local template can be called as follows:
150
+ #
151
+ # HTree.expand_template{<<'End'}
152
+ # <a _template=ruby_talk(num)
153
+ # _attr_href='"http://ruby-talk.org/#{num}"'
154
+ # >[ruby-talk:<span _text=num>nnn</span>]</a>
155
+ # Ruby 1.8.0 is released at <span _call=ruby_talk(77946) />.
156
+ # Ruby 1.8.1 is released at <span _call=ruby_talk(88814) />.
157
+ # End
158
+ #
159
+ # <i>mod</i> should be the result of HTree.compile_template.
160
+ #
161
+ # M = HTree.compile_template(<<'End')
162
+ # <a _template=ruby_talk(num)
163
+ # _attr_href='"http://ruby-talk.org/#{num}"'
164
+ # >[ruby-talk:<span _text=num>nnn</span>]</a>
165
+ # End
166
+ # HTree.expand_template{<<'End'}
167
+ # <html>
168
+ # Ruby 1.8.0 is released at <span _call=M.ruby_talk(77946) />.
169
+ # Ruby 1.8.1 is released at <span _call=M.ruby_talk(88814) />.
170
+ # </html>
171
+ # End
172
+ #
173
+ # The module can included.
174
+ # In such case, the template function can be called without <i>mod.</i>
175
+ # prefix.
176
+ #
177
+ # include HTree.compile_template(<<'End')
178
+ # <a _template=ruby_talk(num)
179
+ # _attr_href='"http://ruby-talk.org/#{num}"'
180
+ # >[ruby-talk:<span _text=num>nnn</span>]</a>
181
+ # End
182
+ # HTree.expand_template{<<'End'}
183
+ # <html>
184
+ # Ruby 1.8.0 is released at <span _call=ruby_talk(77946) />.
185
+ # Ruby 1.8.1 is released at <span _call=ruby_talk(88814) />.
186
+ # </html>
187
+ # End
188
+ #
189
+ # - template definition
190
+ # - <elem _template="<i>name(vars)</i>">body</elem>
191
+ #
192
+ # _template defines a template function which is usable by _call.
193
+ #
194
+ # When a template is compiled to a module by HTree.compile_template,
195
+ # the module have a module function for each template function
196
+ # defined by outermost _template attribute.
197
+ #
198
+ # === White Space Handling
199
+ #
200
+ # The htree template engine strips whitespace text nodes in a template
201
+ # except under HTML pre element.
202
+ #
203
+ # For example the white space text node between two spans in following template is stripped.
204
+ #
205
+ # <span _text="'a'"/> <span _text="'b'"/> -> "ab"
206
+ #
207
+ # Character entity references are not stripped.
208
+ #
209
+ # <span _text="'a'"/>&#32;<span _text="'b'"/> -> "a&#32;b"
210
+ #
211
+ # Text nodes generated by _text is not stripped.
212
+ #
213
+ # <span _text="'a'"/><span _text="' '"> </span><span _text="'b'"/> -> "a b"
214
+ #
215
+ # == HTML and XML
216
+ #
217
+ # The htree template engine outputs HTML or XML.
218
+ #
219
+ # If a template has no XML declaration and the top element is HTML,
220
+ # the result is HTML.
221
+ # Otherwise the result is XML.
222
+ #
223
+ # They differs as follows.
224
+ #
225
+ # - XML declaration is (re-)generated for XML.
226
+ # - empty elements ends with a slash for XML.
227
+ # - script and style element is escaped for XML.
228
+ #
229
+ # == Design Decision on Design/Logic Separation
230
+ #
231
+ # HTree template engine doesn't force you to separate design and logic.
232
+ # Any logic (Ruby code) can be embedded in design (HTML).
233
+ #
234
+ # However the template engine cares the separation by logic refactorings.
235
+ # The logic is easy to move between a template and an application.
236
+ # For example, following tangled template
237
+ #
238
+ # tmpl.html:
239
+ # <html>
240
+ # <head>
241
+ # <title _text="very-complex-ruby-code">dummy</title>
242
+ # </head>
243
+ # ...
244
+ # </html>
245
+ #
246
+ # app.rb:
247
+ # HTree.expand_template('tmpl.html', obj)
248
+ #
249
+ # can be refactored as follows.
250
+ #
251
+ # tmpl.html:
252
+ # <html>
253
+ # <head>
254
+ # <title _text="title">dummy</title>
255
+ # </head>
256
+ # ...
257
+ # </html>
258
+ #
259
+ # app.rb:
260
+ # def obj.title
261
+ # very-complex-ruby-code
262
+ # end
263
+ # HTree.expand_template('tmpl.html', obj)
264
+ #
265
+ # In general, any expression in a template can be refactored to an application
266
+ # by extracting it as a method.
267
+ # In JSP, this is difficult especially for a code fragment of an iteration.
268
+ #
269
+ # Also HTree encourages to separate business logic (Ruby code in an application)
270
+ # and presentation logic (Ruby code in a template).
271
+ # For example, presentation logic to color table rows stripe
272
+ # can be embedded in a template.
273
+ # It doesn't need to tangle an application.
274
+ #
275
+
276
+ module HTree
277
+ # :stopdoc:
278
+ EmptyBindingObject = Object.new
279
+ # :startdoc:
280
+ end
281
+ # :stopdoc:
282
+ def (HTree::EmptyBindingObject).empty_binding
283
+ binding
284
+ end
285
+ # :startdoc:
286
+
287
+ require 'htree/parse'
288
+ require 'htree/gencode'
289
+ require 'htree/equality'
290
+ require 'htree/traverse'
291
+
292
+ # call-seq:
293
+ # HTree.expand_template(template_pathname, obj=Object.new, out=$stdout, encoding=internal_encoding) -> out
294
+ # HTree.expand_template(out=$stdout, encoding=internal_encoding) { template_string } -> out
295
+ #
296
+ # <code>HTree.expand_template</code> expands a template.
297
+ #
298
+ # The arguments should be specified as follows.
299
+ # All argument except <i>pathname</i> are optional.
300
+ #
301
+ # - HTree.expand_template(<i>pathname</i>, <i>obj</i>, <i>out</i>, <i>encoding</i>) -> <i>out</i>
302
+ # - HTree.expand_template(<i>out</i>, <i>encoding</i>) {<i>template_string</i>} -> <i>out</i>
303
+ #
304
+ # The template is specified by a file or a string.
305
+ # If a block is not given, the first argument represent a template pathname.
306
+ # Otherwise, the block is yielded and its value is interpreted as a template
307
+ # string.
308
+ # So it can be called as follows in simplest case.
309
+ #
310
+ # - HTree.expand_template(<i>template_pathname</i>)
311
+ # - HTree.expand_template{<i>template_string</i>}
312
+ #
313
+ # Ruby expressions in the template file specified by _template_pathname_ are
314
+ # evaluated in the context of the optional second argument <i>obj</i> as follows.
315
+ # I.e. the pseudo variable self in the expressions is bound to <i>obj</i>.
316
+ #
317
+ # HTree.expand_template(template_pathname, obj)
318
+ #
319
+ # Ruby expressions in the template_string are evaluated
320
+ # in the context of the caller of HTree.expand_template.
321
+ # (binding information is specified by the block.)
322
+ # I.e. they can access local variables etc.
323
+ # We recommend to specify template_string as a literal string without
324
+ # interpolation because dynamically generated string may break lexical scope.
325
+ #
326
+ # HTree.expand_template has two more optional arguments:
327
+ # <i>out</i>, <i>encoding</i>.
328
+ #
329
+ # <i>out</i> specifies output target.
330
+ # It should have <tt><<</tt> method: IO and String for example.
331
+ # If it is not specified, $stdout is used.
332
+ # If it has a method <tt>charset=</tt>, it is called to set the minimal charset
333
+ # of the result before <tt><<</tt> is called.
334
+ #
335
+ # <i>encoding</i> specifies output character encoding.
336
+ # If it is not specified, internal encoding is used.
337
+ #
338
+ # HTree.expand_template returns <i>out</i> or $stdout if <i>out</i> is not
339
+ # specified.
340
+ #
341
+ def HTree.expand_template(*args, &block)
342
+ if block
343
+ template = block.call
344
+ binding = block.binding
345
+ else
346
+ pathname = args.fetch(0) { raise ArgumentError, "pathname not given" }
347
+ args.shift
348
+ obj = args.fetch(0) { Object.new }
349
+ args.shift
350
+ if pathname.respond_to? :read
351
+ template = pathname.read.untaint
352
+ if template.respond_to? :charset
353
+ template = Iconv.conv(HTree::Encoder.internal_charset, template.charset, template)
354
+ end
355
+ else
356
+ template = File.read(pathname).untaint
357
+ end
358
+ Thread.current[:htree_expand_template_obj] = obj
359
+ binding = eval(<<-'End',
360
+ Thread.current[:htree_expand_template_obj].class.class_eval <<-'EE'
361
+ Thread.current[:htree_expand_template_obj].instance_eval { binding }
362
+ EE
363
+ End
364
+ HTree::EmptyBindingObject.empty_binding, "(eval:#{__FILE__}:#{__LINE__})")
365
+ Thread.current[:htree_expand_template_obj] = nil
366
+ end
367
+
368
+ out = args.shift || $stdout
369
+ encoding = args.shift || HTree::Encoder.internal_charset
370
+ if !args.empty?
371
+ raise ArgumentError, "wrong number of arguments"
372
+ end
373
+ HTree::TemplateCompiler.new.expand_template(template, out, encoding, binding)
374
+ end
375
+
376
+ # call-seq:
377
+ # HTree(html_string) -> doc
378
+ # HTree{template_string} -> doc
379
+ #
380
+ # <code>HTree(<i>html_string</i>)</code> parses <i>html_string</i>.
381
+ # <code>HTree{<i>template_string</i>}</code> parses <i>template_string</i> and expand it as a template.
382
+ # Ruby expressions in <i>template_string</i> is evaluated in the scope of the caller.
383
+ #
384
+ # <code>HTree()</code> and <code>HTree{}</code> returns a tree as an instance of HTree::Doc.
385
+ def HTree(html_string=nil, &block)
386
+ if block_given?
387
+ raise ArgumentError, "both argument and block given." if html_string
388
+ template = block.call
389
+ HTree.parse(HTree::TemplateCompiler.new.expand_template(template, '', HTree::Encoder.internal_charset, block.binding))
390
+ else
391
+ HTree.parse(html_string)
392
+ end
393
+ end
394
+
395
+ # call-seq:
396
+ # HTree.compile_template(template_string) -> module
397
+ #
398
+ # <code>HTree.compile_template(<i>template_string</i>)</code> compiles
399
+ # <i>template_string</i> as a template.
400
+ #
401
+ # HTree.compile_template returns a module.
402
+ # The module has module functions for each templates defined in
403
+ # <i>template_string</i>.
404
+ # The returned module can be used for +include+.
405
+ #
406
+ # M = HTree.compile_template(<<'End')
407
+ # <p _template=birthday(subj,t)>
408
+ # <span _text=subj />'s birthday is <span _text="t.strftime('%B %dth %Y')"/>.
409
+ # </p>
410
+ # End
411
+ # M.birthday('Ruby', Time.utc(1993, 2, 24)).display_xml
412
+ # # <p>Ruby's birthday is February 24th 1993.</p>
413
+ #
414
+ # The module function takes arguments specifies by a <code>_template</code>
415
+ # attribute and returns a tree represented as HTree::Node.
416
+ #
417
+ def HTree.compile_template(template_string)
418
+ code = HTree::TemplateCompiler.new.compile_template(template_string)
419
+ Thread.current[:htree_compile_template_code] = code
420
+ mod = eval(<<-'End',
421
+ eval(Thread.current[:htree_compile_template_code])
422
+ End
423
+ HTree::EmptyBindingObject.empty_binding, "(eval:#{__FILE__}:#{__LINE__})")
424
+ Thread.current[:htree_compile_template_code] = nil
425
+ mod
426
+ end
427
+
428
+ # :stopdoc:
429
+
430
+ class HTree::TemplateCompiler
431
+ IGNORABLE_ELEMENTS = {
432
+ 'span' => true,
433
+ 'div' => true,
434
+ '{http://www.w3.org/1999/xhtml}span' => true,
435
+ '{http://www.w3.org/1999/xhtml}div' => true,
436
+ }
437
+
438
+ def initialize
439
+ @gensym_id = 0
440
+ end
441
+
442
+ def gensym(suffix='')
443
+ @gensym_id += 1
444
+ "g#{@gensym_id}#{suffix}"
445
+ end
446
+
447
+ def parse_template(template)
448
+ strip_whitespaces(HTree.parse(template))
449
+ end
450
+
451
+ WhiteSpacePreservingElements = {
452
+ '{http://www.w3.org/1999/xhtml}pre' => true
453
+ }
454
+
455
+ def strip_whitespaces(template)
456
+ case template
457
+ when HTree::Doc
458
+ HTree::Doc.new(*template.children.map {|c| strip_whitespaces(c) }.compact)
459
+ when HTree::Elem, HTree::Doc
460
+ return template if WhiteSpacePreservingElements[template.name]
461
+ subst = {}
462
+ template.children.each_with_index {|c, i|
463
+ subst[i] = strip_whitespaces(c)
464
+ }
465
+ template.subst_subnode(subst)
466
+ when HTree::Text
467
+ if /\A[ \t\r\n]*\z/ =~ template.rcdata
468
+ nil
469
+ else
470
+ template
471
+ end
472
+ else
473
+ template
474
+ end
475
+ end
476
+
477
+ def template_is_html(template)
478
+ template.each_child {|c|
479
+ return false if c.xmldecl?
480
+ return true if c.elem? && c.element_name.namespace_uri == 'http://www.w3.org/1999/xhtml'
481
+ }
482
+ false
483
+ end
484
+
485
+ def expand_template(template, out, encoding, binding)
486
+ template = parse_template(template)
487
+ is_html = template_is_html(template)
488
+ outvar = gensym('out')
489
+ contextvar = gensym('top_context')
490
+ code = ''
491
+ code << "#{outvar} = HTree::Encoder.new(#{encoding.dump})\n"
492
+ code << "#{outvar}.html_output = true\n" if is_html
493
+ code << "#{contextvar} = #{is_html ? "HTree::HTMLContext" : "HTree::DefaultContext"}\n"
494
+ code << compile_body(outvar, contextvar, template, false)
495
+ code << "[#{outvar}.#{is_html ? "finish" : "finish_with_xmldecl"}, #{outvar}.minimal_charset]\n"
496
+ #puts code; STDOUT.flush
497
+ result, minimal_charset = eval(code, binding, "(eval:#{__FILE__}:#{__LINE__})")
498
+ out.charset = minimal_charset if out.respond_to? :charset=
499
+ out << result
500
+ out
501
+ end
502
+
503
+ def compile_template(src)
504
+ srcdoc = parse_template(src)
505
+ templates = []
506
+ body = extract_templates(srcdoc, templates, true)
507
+ methods = []
508
+ templates.each {|name_args, node|
509
+ methods << compile_global_template(name_args, node)
510
+ }
511
+ <<"End"
512
+ require 'htree/encoder'
513
+ require 'htree/context'
514
+ Module.new.module_eval <<'EE'
515
+ module_function
516
+ #{methods.join('').chomp}
517
+ self
518
+ EE
519
+ End
520
+ end
521
+
522
+ def template_attribute?(name)
523
+ /\A_/ =~ name.local_name
524
+ end
525
+
526
+ def extract_templates(node, templates, is_toplevel)
527
+ case node
528
+ when HTree::Doc
529
+ subst = {}
530
+ node.children.each_with_index {|n, i|
531
+ subst[i] = extract_templates(n, templates, is_toplevel)
532
+ }
533
+ node.subst_subnode(subst)
534
+ when HTree::Elem
535
+ ht_attrs, rest_attrs = node.attributes.partition {|name, text| template_attribute? name }
536
+ if ht_attrs.empty?
537
+ subst = {}
538
+ node.children.each_with_index {|n, i|
539
+ subst[i] = extract_templates(n, templates, is_toplevel)
540
+ }
541
+ node.subst_subnode(subst)
542
+ else
543
+ ht_attrs.each {|htname, text|
544
+ if htname.universal_name == '_template'
545
+ name_fargs = text.to_s
546
+ templates << [name_fargs, node.subst_subnode('_template' => nil)]
547
+ return nil
548
+ end
549
+ }
550
+ if is_toplevel
551
+ raise HTree::Error, "unexpected template attributes in toplevel: #{ht_attrs.inspect}"
552
+ else
553
+ node
554
+ end
555
+ end
556
+ else
557
+ node
558
+ end
559
+ end
560
+
561
+ ID_PAT = /[a-z][a-z0-9_]*/
562
+ NAME_FARGS_PAT = /(#{ID_PAT})(?:\(\s*(|#{ID_PAT}\s*(?:,\s*#{ID_PAT}\s*)*)\))?/
563
+ def compile_global_template(name_fargs, node)
564
+ unless /\A#{NAME_FARGS_PAT}\z/o =~ name_fargs
565
+ raise HTree::Error, "invalid template declaration: #{name_fargs}"
566
+ end
567
+ name = $1
568
+ fargs = $2 ? $2.scan(ID_PAT) : []
569
+
570
+ outvar = gensym('out')
571
+ contextvar = gensym('top_context')
572
+ args2 = [outvar, contextvar, *fargs]
573
+
574
+ <<"End"
575
+ def #{name}(#{fargs.join(',')})
576
+ HTree.parse(_xml_#{name}(#{fargs.join(',')}))
577
+ end
578
+ def _xml_#{name}(#{fargs.join(',')})
579
+ #{outvar} = HTree::Encoder.new(HTree::Encoder.internal_charset)
580
+ #{contextvar} = HTree::DefaultContext
581
+ _ht_#{name}(#{args2.join(',')})
582
+ #{outvar}.finish
583
+ end
584
+ def _ht_#{name}(#{args2.join(',')})
585
+ #{compile_body(outvar, contextvar, node, false)}\
586
+ end
587
+ public :_ht_#{name}
588
+ End
589
+ end
590
+
591
+ def compile_local_template(name_fargs, node, local_templates)
592
+ unless /\A#{NAME_FARGS_PAT}\z/o =~ name_fargs
593
+ raise HTree::Error, "invalid template declaration: #{name_fargs}"
594
+ end
595
+ name = $1
596
+ fargs = $2 ? $2.scan(ID_PAT) : []
597
+
598
+ outvar = gensym('out')
599
+ contextvar = gensym('top_context')
600
+ args2 = [outvar, contextvar, *fargs]
601
+
602
+ <<"End"
603
+ #{name} = lambda {|#{args2.join(',')}|
604
+ #{compile_body(outvar, contextvar, node, false, local_templates)}\
605
+ }
606
+ End
607
+ end
608
+
609
+ def compile_body(outvar, contextvar, node, is_toplevel, local_templates={})
610
+ if node.elem? && IGNORABLE_ELEMENTS[node.name] && node.attributes.empty?
611
+ node = TemplateNode.new(node.children)
612
+ else
613
+ node = TemplateNode.new(node)
614
+ end
615
+ generate_logic_node([:content], node, local_templates).generate_xml_output_code(outvar, contextvar)
616
+ end
617
+
618
+ def compile_node(node, local_templates)
619
+ case node
620
+ when HTree::Doc
621
+ TemplateNode.new(node.children.map {|n| compile_node(n, local_templates) })
622
+ when HTree::Elem
623
+ ht_attrs = node.attributes.find_all {|name, text| template_attribute? name }
624
+ ht_attrs = ht_attrs.sort_by {|htname, text| htname.universal_name }
625
+ ignore_tag = false
626
+ unless ht_attrs.empty?
627
+ attr_mod = {}
628
+ ht_attrs.each {|htname, text|
629
+ attr_mod[htname] = nil
630
+ if /\A_attr_/ =~ htname.local_name
631
+ attr_mod[TemplateAttrName.new(htname.namespace_prefix, htname.namespace_uri, $')] = text
632
+ end
633
+ }
634
+ ht_attrs.reject! {|htname, text| /\A_attr_/ =~ htname.local_name }
635
+ node = node.subst_subnode(attr_mod)
636
+ ignore_tag = IGNORABLE_ELEMENTS[node.name] && node.attributes.empty?
637
+ end
638
+ ht_names = ht_attrs.map {|htname, text| htname.universal_name }
639
+ ht_vals = ht_attrs.map {|htname, text| text.to_s }
640
+ case ht_names
641
+ when []
642
+ generate_logic_node([:tag, [:content]], node, local_templates)
643
+ when ['_text'] # <n _text="expr" /> or <n _text>expr</n>
644
+ if ht_vals[0] != '_text' # xxx: attribute value is really omitted?
645
+ expr = ht_vals[0]
646
+ else
647
+ children = node.children
648
+ if children.length != 1
649
+ raise HTree::Error, "_text expression has #{children.length} nodes"
650
+ end
651
+ if !children[0].text?
652
+ raise HTree::Error, "_text expression is not text: #{children[0].class}"
653
+ end
654
+ expr = children[0].to_s
655
+ end
656
+ if ignore_tag && /\A\s*'((?:[^'\\]|\\.)*)'\s*\z/m =~ expr
657
+ # if expr is just a constant string literal, use it as a literal text.
658
+ # This saves dynamic evaluation of <span _text="' '"/>
659
+ # xxx: handle "..." as well if it has no #{}.
660
+ HTree::Text.new($1.gsub(/\\(.)/m, '\1'))
661
+ else
662
+ generate_logic_node(compile_dynamic_text(ignore_tag, expr), node, local_templates)
663
+ end
664
+ when ['_tree'] # <n _tree="expr" /> or <n _tree>expr</n>
665
+ if ht_vals[0] != '_tree' # xxx: attribute value is really omitted?
666
+ expr = ht_vals[0]
667
+ else
668
+ children = node.children
669
+ if children.length != 1
670
+ raise HTree::Error, "_tree expression has #{children.length} nodes"
671
+ end
672
+ if !children[0].text?
673
+ raise HTree::Error, "_tree expression is not text: #{children[0].class}"
674
+ end
675
+ expr = children[0].to_s
676
+ end
677
+ generate_logic_node(compile_dynamic_tree(ignore_tag, expr), node, local_templates)
678
+ when ['_if'] # <n _if="expr" >...</n>
679
+ generate_logic_node(compile_if(ignore_tag, ht_vals[0], nil), node, local_templates)
680
+ when ['_else', '_if'] # <n _if="expr" _else="expr.meth(args)" >...</n>
681
+ generate_logic_node(compile_if(ignore_tag, ht_vals[1], ht_vals[0]), node, local_templates)
682
+ when ['_call'] # <n _call="recv.meth(args)" />
683
+ generate_logic_node(compile_call(ignore_tag, ht_vals[0]), node, local_templates)
684
+ when ['_iter'] # <n _iter="expr.meth(args)//fargs" >...</n>
685
+ generate_logic_node(compile_iter(ignore_tag, ht_vals[0]), node, local_templates)
686
+ when ['_iter_content'] # <n _iter_content="expr.meth(args)//fargs" >...</n>
687
+ generate_logic_node(compile_iter_content(ignore_tag, ht_vals[0]), node, local_templates)
688
+ else
689
+ raise HTree::Error, "unexpected template attributes: #{ht_attrs.inspect}"
690
+ end
691
+ else
692
+ return node
693
+ end
694
+ end
695
+
696
+ def valid_syntax?(code)
697
+ begin
698
+ eval("BEGIN {return true}\n#{code.untaint}")
699
+ rescue SyntaxError
700
+ raise SyntaxError, "invalid code: #{code}"
701
+ end
702
+ end
703
+
704
+ def check_syntax(code)
705
+ unless valid_syntax?(code)
706
+ raise HTree::Error, "invalid ruby code: #{code}"
707
+ end
708
+ end
709
+
710
+ def compile_dynamic_text(ignore_tag, expr)
711
+ check_syntax(expr)
712
+ logic = [:text, expr]
713
+ logic = [:tag, logic] unless ignore_tag
714
+ logic
715
+ end
716
+
717
+ def compile_dynamic_tree(ignore_tag, expr)
718
+ check_syntax(expr)
719
+ logic = [:tree, expr]
720
+ logic = [:tag, logic] unless ignore_tag
721
+ logic
722
+ end
723
+
724
+ def compile_if(ignore_tag, expr, else_call)
725
+ check_syntax(expr)
726
+ then_logic = [:content]
727
+ unless ignore_tag
728
+ then_logic = [:tag, then_logic]
729
+ end
730
+ else_logic = nil
731
+ if else_call
732
+ else_logic = compile_call(true, else_call)
733
+ end
734
+ [:if, expr, then_logic, else_logic]
735
+ end
736
+
737
+ def split_args(spec)
738
+ return spec, '' if /\)\z/ !~ spec
739
+ i = spec.length - 1
740
+ nest = 0
741
+ begin
742
+ raise HTree::Error, "unmatched paren: #{spec}" if i < 0
743
+ case spec[i]
744
+ when ?\)
745
+ nest += 1
746
+ when ?\(
747
+ nest -= 1
748
+ end
749
+ i -= 1
750
+ end while nest != 0
751
+ i += 1
752
+ return spec[0, i], spec[(i+1)...-1]
753
+ end
754
+
755
+ def compile_call(ignore_tag, spec)
756
+ # spec : [recv.]meth[(args)]
757
+ spec = spec.strip
758
+ spec, args = split_args(spec)
759
+ unless /#{ID_PAT}\z/o =~ spec
760
+ raise HTree::Error, "invalid _call: #{spec}"
761
+ end
762
+ meth = $&
763
+ spec = $`
764
+ if /\A\s*\z/ =~ spec
765
+ recv = nil
766
+ elsif /\A\s*(.*)\.\z/ =~ spec
767
+ recv = $1
768
+ else
769
+ raise HTree::Error, "invalid _call: #{spec}"
770
+ end
771
+ if recv
772
+ check_syntax(recv)
773
+ check_syntax("#{recv}.#{meth}(#{args})")
774
+ end
775
+ check_syntax("#{meth}(#{args})")
776
+ [:call, recv, meth, args]
777
+ end
778
+
779
+ def compile_iter(ignore_tag, spec)
780
+ # spec: <n _iter="expr.meth[(args)]//fargs" >...</n>
781
+ spec = spec.strip
782
+ unless %r{\s*//\s*(#{ID_PAT}\s*(?:,\s*#{ID_PAT}\s*)*)?\z}o =~ spec
783
+ raise HTree::Error, "invalid block arguments for _iter: #{spec}"
784
+ end
785
+ call = $`.strip
786
+ fargs = $1 ? $1.strip : ''
787
+ check_syntax("#{call} {|#{fargs}| }")
788
+ logic = [:content]
789
+ unless ignore_tag
790
+ logic = [:tag, logic]
791
+ end
792
+ [:iter, call, fargs, logic]
793
+ end
794
+
795
+ def compile_iter_content(ignore_tag, spec)
796
+ # spec: <n _iter_content="expr.meth[(args)]//fargs" >...</n>
797
+ spec = spec.strip
798
+ unless %r{\s*//\s*(#{ID_PAT}\s*(?:,\s*#{ID_PAT}\s*)*)?\z}o =~ spec
799
+ raise HTree::Error, "invalid block arguments for _iter: #{spec}"
800
+ end
801
+ call = $`.strip
802
+ fargs = $1 ? $1.strip : ''
803
+ check_syntax("#{call} {|#{fargs}| }")
804
+ logic = [:content]
805
+ logic = [:iter, call, fargs, logic]
806
+ unless ignore_tag
807
+ logic = [:tag, logic]
808
+ end
809
+ logic
810
+ end
811
+
812
+ def generate_logic_node(logic, node, local_templates)
813
+ # logic ::= [:if, expr, then_logic, else_logic]
814
+ # | [:iter, call, fargs, logic]
815
+ # | [:tag, logic]
816
+ # | [:text, expr]
817
+ # | [:tree, expr]
818
+ # | [:call, expr, meth, args]
819
+ # | [:content]
820
+ # | [:empty]
821
+ case logic.first
822
+ when :empty
823
+ nil
824
+ when :content
825
+ subtemplates = []
826
+ children = []
827
+ node.children.each {|c|
828
+ children << extract_templates(c, subtemplates, false)
829
+ }
830
+ if subtemplates.empty?
831
+ TemplateNode.new(node.children.map {|n|
832
+ compile_node(n, local_templates)
833
+ })
834
+ else
835
+ local_templates = local_templates.dup
836
+ decl = ''
837
+ subtemplates.each {|sub_name_args, sub_node|
838
+ sub_name = sub_name_args[ID_PAT]
839
+ local_templates[sub_name] = sub_name
840
+ decl << "#{sub_name} = "
841
+ }
842
+ decl << "nil\n"
843
+ defs = []
844
+ subtemplates.each {|sub_name_args, sub_node|
845
+ defs << lambda {|out, context|
846
+ out.output_logic_line compile_local_template(sub_name_args, sub_node, local_templates)
847
+ }
848
+ }
849
+ TemplateNode.new(
850
+ lambda {|out, context| out.output_logic_line decl },
851
+ defs,
852
+ children.map {|n| compile_node(n, local_templates) }
853
+ )
854
+ end
855
+ when :text
856
+ _, expr = logic
857
+ TemplateNode.new(lambda {|out, context| out.output_dynamic_text expr })
858
+ when :tree
859
+ _, expr = logic
860
+ TemplateNode.new(lambda {|out, context| out.output_dynamic_tree expr, make_context_expr(out, context) })
861
+ when :tag
862
+ _, rest_logic = logic
863
+ if rest_logic == [:content] && node.empty_element?
864
+ node
865
+ else
866
+ subst = {}
867
+ node.children.each_index {|i| subst[i] = nil }
868
+ subst[0] = TemplateNode.new(generate_logic_node(rest_logic, node, local_templates))
869
+ node.subst_subnode(subst)
870
+ end
871
+ when :if
872
+ _, expr, then_logic, else_logic = logic
873
+ children = [
874
+ lambda {|out, context| out.output_logic_line "if (#{expr})" },
875
+ generate_logic_node(then_logic, node, local_templates)
876
+ ]
877
+ if else_logic
878
+ children.concat [
879
+ lambda {|out, context| out.output_logic_line "else" },
880
+ generate_logic_node(else_logic, node, local_templates)
881
+ ]
882
+ end
883
+ children <<
884
+ lambda {|out, context| out.output_logic_line "end" }
885
+ TemplateNode.new(*children)
886
+ when :iter
887
+ _, call, fargs, rest_logic = logic
888
+ TemplateNode.new(
889
+ lambda {|out, context| out.output_logic_line "#{call} {|#{fargs}|" },
890
+ generate_logic_node(rest_logic, node, local_templates),
891
+ lambda {|out, context| out.output_logic_line "}" }
892
+ )
893
+ when :call
894
+ _, recv, meth, args = logic
895
+ TemplateNode.new(
896
+ lambda {|out, context|
897
+ as = [out.outvar, ", ", make_context_expr(out, context)]
898
+ unless args.empty?
899
+ as << ", " << args
900
+ end
901
+ if recv
902
+ out.output_logic_line "(#{recv})._ht_#{meth}(#{as.join('')})"
903
+ elsif local_templates.include? meth
904
+ out.output_logic_line "#{meth}.call(#{as.join('')})"
905
+ else
906
+ out.output_logic_line "_ht_#{meth}(#{as.join('')})"
907
+ end
908
+ }
909
+ )
910
+ else
911
+ raise Exception, "[bug] invalid logic: #{logic.inspect}"
912
+ end
913
+ end
914
+
915
+ def make_context_expr(out, context)
916
+ ns = context.namespaces.reject {|k, v| HTree::Context::DefaultNamespaces[k] == v }
917
+ if ns.empty?
918
+ result = out.contextvar
919
+ else
920
+ result = "#{out.contextvar}.subst_namespaces("
921
+ sep = ''
922
+ ns.each {|k, v|
923
+ result << sep << (k ? k.dump : "nil") << '=>' << v.dump
924
+ sep = ', '
925
+ }
926
+ result << ")"
927
+ end
928
+ result
929
+ end
930
+
931
+ class TemplateNode
932
+ include HTree::Node
933
+
934
+ def initialize(*children)
935
+ @children = children.flatten.compact
936
+ end
937
+ attr_reader :children
938
+
939
+ def output(out, context)
940
+ @children.each {|c|
941
+ if c.respond_to? :call
942
+ c.call(out, context)
943
+ else
944
+ c.output(out, context)
945
+ end
946
+ }
947
+ end
948
+ end
949
+
950
+ class TemplateAttrName < HTree::Name
951
+ def output_attribute(text, out, context)
952
+ output(out, context)
953
+ out.output_string '="'
954
+ out.output_dynamic_attvalue(text.to_s)
955
+ out.output_string '"'
956
+ end
957
+ end
958
+
959
+ end
960
+
961
+ # :startdoc: