htree 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: