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,115 @@
1
+ require 'htree/modules'
2
+ require 'htree/raw_string'
3
+ require 'htree/htmlinfo'
4
+ require 'htree/encoder'
5
+ require 'htree/fstr'
6
+ require 'iconv'
7
+
8
+ module HTree
9
+ class Text
10
+ # :stopdoc:
11
+ class << self
12
+ alias new_internal new
13
+ end
14
+ # :startdoc:
15
+
16
+ def Text.new(arg)
17
+ arg = arg.to_node if HTree::Location === arg
18
+ if Text === arg
19
+ new_internal arg.rcdata, arg.normalized_rcdata
20
+ elsif String === arg
21
+ arg2 = arg.gsub(/&/, '&amp;')
22
+ arg = arg2.freeze if arg != arg2
23
+ new_internal arg
24
+ else
25
+ raise TypeError, "cannot initialize Text with #{arg.inspect}"
26
+ end
27
+ end
28
+
29
+ def initialize(rcdata, normalized_rcdata=internal_normalize(rcdata)) # :notnew:
30
+ init_raw_string
31
+ @rcdata = rcdata && HTree.frozen_string(rcdata)
32
+ @normalized_rcdata = @rcdata == normalized_rcdata ? @rcdata : normalized_rcdata
33
+ end
34
+ attr_reader :rcdata, :normalized_rcdata
35
+
36
+ def internal_normalize(rcdata)
37
+ # - character references are decoded as much as possible.
38
+ # - undecodable character references are converted to decimal numeric character refereces.
39
+ result = rcdata.gsub(/&(?:#([0-9]+)|#x([0-9a-fA-F]+)|([A-Za-z][A-Za-z0-9]*));/o) {|s|
40
+ u = nil
41
+ if $1
42
+ u = $1.to_i
43
+ elsif $2
44
+ u = $2.hex
45
+ elsif $3
46
+ u = NamedCharacters[$3]
47
+ end
48
+ if !u || u < 0 || 0x7fffffff < u
49
+ '?'
50
+ elsif u == 38 # '&' character.
51
+ '&#38;'
52
+ elsif u <= 0x7f
53
+ [u].pack("C")
54
+ else
55
+ begin
56
+ Iconv.conv(Encoder.internal_charset, 'UTF-8', [u].pack("U"))
57
+ rescue Iconv::Failure
58
+ "&##{u};"
59
+ end
60
+ end
61
+ }
62
+ HTree.frozen_string(result)
63
+ end
64
+ private :internal_normalize
65
+
66
+ # HTree::Text#to_s converts the text to a string.
67
+ # - character references are decoded as much as possible.
68
+ # - undecodable character reference are converted to `?' character.
69
+ def to_s
70
+ @normalized_rcdata.gsub(/&(?:#([0-9]+));/o) {|s|
71
+ u = $1.to_i
72
+ if 0 <= u && u <= 0x7f
73
+ [u].pack("C")
74
+ else
75
+ '?'
76
+ end
77
+ }
78
+ end
79
+
80
+ def empty?
81
+ @normalized_rcdata.empty?
82
+ end
83
+
84
+ def strip
85
+ rcdata = @normalized_rcdata.dup
86
+ rcdata.sub!(/\A(?:\s|&nbsp;)+/, '')
87
+ rcdata.sub!(/(?:\s|&nbsp;)+\z/, '')
88
+ if rcdata == @normalized_rcdata
89
+ self
90
+ else
91
+ rcdata.freeze
92
+ Text.new_internal(rcdata, rcdata)
93
+ end
94
+ end
95
+
96
+ # HTree::Text.concat returns a text which is concatenation of arguments.
97
+ #
98
+ # An argument should be one of follows.
99
+ # - String
100
+ # - HTree::Text
101
+ # - HTree::Location which points HTree::Text
102
+ def Text.concat(*args)
103
+ rcdata = ''
104
+ args.each {|arg|
105
+ arg = arg.to_node if HTree::Location === arg
106
+ if Text === arg
107
+ rcdata << arg.rcdata
108
+ else
109
+ rcdata << arg.gsub(/&/, '&amp;')
110
+ end
111
+ }
112
+ new_internal rcdata
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,497 @@
1
+ require 'htree/doc'
2
+ require 'htree/elem'
3
+ require 'htree/loc'
4
+ require 'htree/extract_text'
5
+ require 'uri'
6
+
7
+ module HTree
8
+ module Traverse
9
+ def doc?() Doc::Trav === self end
10
+ def elem?() Elem::Trav === self end
11
+ def text?() Text::Trav === self end
12
+ def xmldecl?() XMLDecl::Trav === self end
13
+ def doctype?() DocType::Trav === self end
14
+ def procins?() ProcIns::Trav === self end
15
+ def comment?() Comment::Trav === self end
16
+ def bogusetag?() BogusETag::Trav === self end
17
+
18
+ def get_subnode(*indexes)
19
+ n = self
20
+ indexes.each {|index|
21
+ n = n.get_subnode_internal(index)
22
+ }
23
+ n
24
+ end
25
+ end
26
+
27
+ module Container::Trav
28
+ # +each_child+ iterates over each child.
29
+ def each_child(&block) # :yields: child_node
30
+ children.each(&block)
31
+ nil
32
+ end
33
+
34
+ # +each_child_with_index+ iterates over each child.
35
+ def each_child_with_index(&block) # :yields: child_node, index
36
+ children.each_with_index(&block)
37
+ nil
38
+ end
39
+
40
+ # +find_element+ searches an element which universal name is specified by
41
+ # the arguments.
42
+ # It returns nil if not found.
43
+ def find_element(*names)
44
+ traverse_element(*names) {|e| return e }
45
+ nil
46
+ end
47
+
48
+ # +traverse_element+ traverses elements in the tree.
49
+ # It yields elements in depth first order.
50
+ #
51
+ # If _names_ are empty, it yields all elements.
52
+ # If non-empty _names_ are given, it should be list of universal names.
53
+ #
54
+ # A nested element is yielded in depth first order as follows.
55
+ #
56
+ # t = HTree('<a id=0><b><a id=1 /></b><c id=2 /></a>')
57
+ # t.traverse_element("a", "c") {|e| p e}
58
+ # # =>
59
+ # {elem <a id="0"> {elem <b> {emptyelem <a id="1">} </b>} {emptyelem <c id="2">} </a>}
60
+ # {emptyelem <a id="1">}
61
+ # {emptyelem <c id="2">}
62
+ #
63
+ # Universal names are specified as follows.
64
+ #
65
+ # t = HTree(<<'End')
66
+ # <html>
67
+ # <meta name="robots" content="index,nofollow">
68
+ # <meta name="author" content="Who am I?">
69
+ # </html>
70
+ # End
71
+ # t.traverse_element("{http://www.w3.org/1999/xhtml}meta") {|e| p e}
72
+ # # =>
73
+ # {emptyelem <{http://www.w3.org/1999/xhtml}meta name="robots" content="index,nofollow">}
74
+ # {emptyelem <{http://www.w3.org/1999/xhtml}meta name="author" content="Who am I?">}
75
+ #
76
+ def traverse_element(*names, &block) # :yields: element
77
+ if names.empty?
78
+ traverse_all_element(&block)
79
+ else
80
+ name_set = {}
81
+ names.each {|n| name_set[n] = true }
82
+ traverse_some_element(name_set, &block)
83
+ end
84
+ nil
85
+ end
86
+
87
+ def each_hyperlink_attribute
88
+ traverse_element(
89
+ '{http://www.w3.org/1999/xhtml}a',
90
+ '{http://www.w3.org/1999/xhtml}area',
91
+ '{http://www.w3.org/1999/xhtml}link',
92
+ '{http://www.w3.org/1999/xhtml}img',
93
+ '{http://www.w3.org/1999/xhtml}object',
94
+ '{http://www.w3.org/1999/xhtml}q',
95
+ '{http://www.w3.org/1999/xhtml}blockquote',
96
+ '{http://www.w3.org/1999/xhtml}ins',
97
+ '{http://www.w3.org/1999/xhtml}del',
98
+ '{http://www.w3.org/1999/xhtml}form',
99
+ '{http://www.w3.org/1999/xhtml}input',
100
+ '{http://www.w3.org/1999/xhtml}head',
101
+ '{http://www.w3.org/1999/xhtml}base',
102
+ '{http://www.w3.org/1999/xhtml}script') {|elem|
103
+ case elem.name
104
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:base|a|area|link)\z}i
105
+ attrs = ['href']
106
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:img)\z}i
107
+ attrs = ['src', 'longdesc', 'usemap']
108
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:object)\z}i
109
+ attrs = ['classid', 'codebase', 'data', 'usemap']
110
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:q|blockquote|ins|del)\z}i
111
+ attrs = ['cite']
112
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:form)\z}i
113
+ attrs = ['action']
114
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:input)\z}i
115
+ attrs = ['src', 'usemap']
116
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:head)\z}i
117
+ attrs = ['profile']
118
+ when %r{\{http://www.w3.org/1999/xhtml\}(?:script)\z}i
119
+ attrs = ['src', 'for']
120
+ end
121
+ attrs.each {|attr|
122
+ if hyperlink = elem.get_attribute(attr)
123
+ yield elem, attr, hyperlink
124
+ end
125
+ }
126
+ }
127
+ end
128
+ private :each_hyperlink_attribute
129
+
130
+ # +each_hyperlink_uri+ traverses hyperlinks such as HTML href attribute
131
+ # of A element.
132
+ #
133
+ # It yields HTree::Text (or HTree::Loc) and URI for each hyperlink.
134
+ #
135
+ # The URI objects are created with a base URI which is given by
136
+ # HTML BASE element or the argument ((|base_uri|)).
137
+ # +each_hyperlink_uri+ doesn't yields href of the BASE element.
138
+ def each_hyperlink_uri(base_uri=nil) # :yields: hyperlink, uri
139
+ base_uri = URI.parse(base_uri) if String === base_uri
140
+ links = []
141
+ each_hyperlink_attribute {|elem, attr, hyperlink|
142
+ if %r{\{http://www.w3.org/1999/xhtml\}(?:base)\z}i =~ elem.name
143
+ base_uri = URI.parse(hyperlink.to_s)
144
+ else
145
+ links << hyperlink
146
+ end
147
+ }
148
+ if base_uri
149
+ links.each {|hyperlink| yield hyperlink, base_uri + hyperlink.to_s }
150
+ else
151
+ links.each {|hyperlink| yield hyperlink, URI.parse(hyperlink.to_s) }
152
+ end
153
+ end
154
+
155
+ # +each_hyperlink+ traverses hyperlinks such as HTML href attribute
156
+ # of A element.
157
+ #
158
+ # It yields HTree::Text or HTree::Loc.
159
+ #
160
+ # Note that +each_hyperlink+ yields HTML href attribute of BASE element.
161
+ def each_hyperlink # :yields: text
162
+ links = []
163
+ each_hyperlink_attribute {|elem, attr, hyperlink|
164
+ yield hyperlink
165
+ }
166
+ end
167
+
168
+ # +each_uri+ traverses hyperlinks such as HTML href attribute
169
+ # of A element.
170
+ #
171
+ # It yields URI for each hyperlink.
172
+ #
173
+ # The URI objects are created with a base URI which is given by
174
+ # HTML BASE element or the argument ((|base_uri|)).
175
+ def each_uri(base_uri=nil) # :yields: URI
176
+ each_hyperlink_uri(base_uri) {|hyperlink, uri| yield uri }
177
+ end
178
+ end
179
+
180
+ # :stopdoc:
181
+ module Doc::Trav
182
+ def traverse_all_element(&block)
183
+ children.each {|c| c.traverse_all_element(&block) }
184
+ end
185
+ end
186
+
187
+ module Elem::Trav
188
+ def traverse_all_element(&block)
189
+ yield self
190
+ children.each {|c| c.traverse_all_element(&block) }
191
+ end
192
+ end
193
+
194
+ module Leaf::Trav
195
+ def traverse_all_element
196
+ end
197
+ end
198
+
199
+ module Doc::Trav
200
+ def traverse_some_element(name_set, &block)
201
+ children.each {|c| c.traverse_some_element(name_set, &block) }
202
+ end
203
+ end
204
+
205
+ module Elem::Trav
206
+ def traverse_some_element(name_set, &block)
207
+ yield self if name_set.include? self.name
208
+ children.each {|c| c.traverse_some_element(name_set, &block) }
209
+ end
210
+ end
211
+
212
+ module Leaf::Trav
213
+ def traverse_some_element(name_set)
214
+ end
215
+ end
216
+ # :startdoc:
217
+
218
+ module Traverse
219
+ # +traverse_text+ traverses texts in the tree
220
+ def traverse_text(&block) # :yields: text
221
+ traverse_text_internal(&block)
222
+ nil
223
+ end
224
+ end
225
+
226
+ # :stopdoc:
227
+ module Container::Trav
228
+ def traverse_text_internal(&block)
229
+ each_child {|c| c.traverse_text_internal(&block) }
230
+ end
231
+ end
232
+
233
+ module Leaf::Trav
234
+ def traverse_text_internal
235
+ end
236
+ end
237
+
238
+ module Text::Trav
239
+ def traverse_text_internal
240
+ yield self
241
+ end
242
+ end
243
+ # :startdoc:
244
+
245
+ module Container::Trav
246
+ # +filter+ rebuilds the tree without some components.
247
+ #
248
+ # node.filter {|descendant_node| predicate } -> node
249
+ # loc.filter {|descendant_loc| predicate } -> node
250
+ #
251
+ # +filter+ yields each node except top node.
252
+ # If given block returns false, corresponding node is dropped.
253
+ # If given block returns true, corresponding node is retained and
254
+ # inner nodes are examined.
255
+ #
256
+ # +filter+ returns an node.
257
+ # It doesn't return location object even if self is location object.
258
+ #
259
+ def filter(&block)
260
+ subst = {}
261
+ each_child_with_index {|descendant, i|
262
+ if yield descendant
263
+ if descendant.elem?
264
+ subst[i] = descendant.filter(&block)
265
+ else
266
+ subst[i] = descendant
267
+ end
268
+ else
269
+ subst[i] = nil
270
+ end
271
+ }
272
+ to_node.subst_subnode(subst)
273
+ end
274
+ end
275
+
276
+ module Doc::Trav
277
+ # +title+ searches title and return it as a text.
278
+ # It returns nil if not found.
279
+ #
280
+ # +title+ searchs following information.
281
+ #
282
+ # - <title>...</title> in HTML
283
+ # - <title>...</title> in RSS
284
+ # - <title>...</title> in Atom
285
+ def title
286
+ e = find_element('title',
287
+ '{http://www.w3.org/1999/xhtml}title',
288
+ '{http://purl.org/rss/1.0/}title',
289
+ '{http://my.netscape.com/rdf/simple/0.9/}title',
290
+ '{http://www.w3.org/2005/Atom}title',
291
+ '{http://purl.org/atom/ns#}title')
292
+ e && e.extract_text
293
+ end
294
+
295
+ # +author+ searches author and return it as a text.
296
+ # It returns nil if not found.
297
+ #
298
+ # +author+ searchs following information.
299
+ #
300
+ # - <meta name="author" content="author-name"> in HTML
301
+ # - <link rev="made" title="author-name"> in HTML
302
+ # - <dc:creator>author-name</dc:creator> in RSS
303
+ # - <dc:publisher>author-name</dc:publisher> in RSS
304
+ # - <author><name>author-name</name></author> in Atom
305
+ def author
306
+ traverse_element('meta',
307
+ '{http://www.w3.org/1999/xhtml}meta') {|e|
308
+ begin
309
+ next unless e.fetch_attr('name').downcase == 'author'
310
+ author = e.fetch_attribute('content').strip
311
+ return author if !author.empty?
312
+ rescue IndexError
313
+ end
314
+ }
315
+
316
+ traverse_element('link',
317
+ '{http://www.w3.org/1999/xhtml}link') {|e|
318
+ begin
319
+ next unless e.fetch_attr('rev').downcase == 'made'
320
+ author = e.fetch_attribute('title').strip
321
+ return author if !author.empty?
322
+ rescue IndexError
323
+ end
324
+ }
325
+
326
+ if channel = find_element('{http://purl.org/rss/1.0/}channel')
327
+ channel.traverse_element('{http://purl.org/dc/elements/1.1/}creator') {|e|
328
+ begin
329
+ author = e.extract_text.strip
330
+ return author if !author.empty?
331
+ rescue IndexError
332
+ end
333
+ }
334
+ channel.traverse_element('{http://purl.org/dc/elements/1.1/}publisher') {|e|
335
+ begin
336
+ author = e.extract_text.strip
337
+ return author if !author.empty?
338
+ rescue IndexError
339
+ end
340
+ }
341
+ end
342
+
343
+ ['http://www.w3.org/2005/Atom', 'http://purl.org/atom/ns#'].each {|xmlns|
344
+ each_child {|top|
345
+ next unless top.elem?
346
+ if top.name == "{#{xmlns}}feed"
347
+ if feed_author = find_element("{#{xmlns}}author")
348
+ feed_author.traverse_element("{#{xmlns}}name") {|e|
349
+ begin
350
+ author = e.extract_text.strip
351
+ return author if !author.empty?
352
+ rescue IndexError
353
+ end
354
+ }
355
+ end
356
+ end
357
+ }
358
+ }
359
+
360
+ nil
361
+ end
362
+
363
+ end
364
+
365
+ module Doc::Trav
366
+ # +root+ searches root element.
367
+ # If there is no element on top level, it raise HTree::Error.
368
+ # If there is two or more elements on top level, it raise HTree::Error.
369
+ def root
370
+ es = []
371
+ children.each {|c| es << c if c.elem? }
372
+ raise HTree::Error, "no element" if es.empty?
373
+ raise HTree::Error, "multiple top elements" if 1 < es.length
374
+ es[0]
375
+ end
376
+
377
+ # +has_xmldecl?+ returns true if there is an XML declaration on top level.
378
+ def has_xmldecl?
379
+ children.each {|c| return true if c.xmldecl? }
380
+ false
381
+ end
382
+ end
383
+
384
+ module Elem::Trav
385
+ # +name+ returns the universal name of the element as a string.
386
+ #
387
+ # p HTree('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"/>').root.name
388
+ # # =>
389
+ # "{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF"
390
+ #
391
+ def name() element_name.universal_name end
392
+
393
+ # +qualified_name+ returns the qualified name of the element as a string.
394
+ #
395
+ # p HTree('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"/>').root.qualified_name
396
+ # # =>
397
+ # "rdf:RDF"
398
+ def qualified_name() element_name.qualified_name end
399
+
400
+ # +attributes+ returns attributes as a hash.
401
+ # The hash keys are HTree::Name objects.
402
+ # The hash values are HTree::Text or HTree::Location objects.
403
+ #
404
+ # p HTree('<a name="xx" href="uu">').root.attributes
405
+ # # =>
406
+ # {href=>{text "uu"}, name=>{text "xx"}}
407
+ #
408
+ # p HTree('<a name="xx" href="uu">').make_loc.root.attributes
409
+ # # =>
410
+ # {href=>#<HTree::Location: doc()/a/@href>, name=>#<HTree::Location: doc()/a/@name>}
411
+ #
412
+ def attributes
413
+ result = {}
414
+ each_attribute {|name, text|
415
+ result[name] = text
416
+ }
417
+ result
418
+ end
419
+
420
+ def each_attr
421
+ each_attribute {|name, text|
422
+ uname = name.universal_name
423
+ str = text.to_s
424
+ yield uname, str
425
+ }
426
+ end
427
+
428
+ # call-seq:
429
+ # elem.fetch_attribute(name) -> text or raise IndexError
430
+ # elem.fetch_attribute(name, default) -> text or default
431
+ # elem.fetch_attribute(name) {|uname| default } -> text or default
432
+ #
433
+ # +fetch_attribute+ returns an attribute value as a text.
434
+ #
435
+ # elem may be an instance of HTree::Elem or a location points to it.
436
+ def fetch_attribute(uname, *rest, &block)
437
+ if 1 < rest.length
438
+ raise ArgumentError, "wrong number of arguments (#{1+rest.length} for 2)"
439
+ end
440
+ if !rest.empty? && block_given?
441
+ raise ArgumentError, "block supersedes default value argument"
442
+ end
443
+ uname = uname.universal_name if uname.respond_to? :universal_name
444
+ return update_attribute_hash.fetch(uname) {
445
+ if block_given?
446
+ return yield(uname)
447
+ elsif !rest.empty?
448
+ return rest[0]
449
+ else
450
+ raise IndexError, "attribute not found: #{uname.inspect}"
451
+ end
452
+ }
453
+ end
454
+
455
+ # call-seq:
456
+ # elem.fetch_attr(name) -> string or raise IndexError
457
+ # elem.fetch_attr(name, default) -> string or default
458
+ # elem.fetch_attr(name) {|uname| default } -> string or default
459
+ #
460
+ # +fetch_attr+ returns an attribute value as a string.
461
+ #
462
+ # elem may be an instance of HTree::Elem or a location points to it.
463
+ def fetch_attr(uname, *rest, &block)
464
+ if 1 < rest.length
465
+ raise ArgumentError, "wrong number of arguments (#{1+rest.length} for 2)"
466
+ end
467
+ if !rest.empty? && block_given?
468
+ raise ArgumentError, "block supersedes default value argument"
469
+ end
470
+ uname = uname.universal_name if uname.respond_to? :universal_name
471
+ return update_attribute_hash.fetch(uname) {
472
+ if block_given?
473
+ return yield(uname)
474
+ elsif !rest.empty?
475
+ return rest[0]
476
+ else
477
+ raise IndexError, "attribute not found: #{uname.inspect}"
478
+ end
479
+ }.to_s
480
+ end
481
+
482
+ def get_attribute(uname)
483
+ uname = uname.universal_name if uname.respond_to? :universal_name
484
+ update_attribute_hash[uname]
485
+ end
486
+
487
+ def get_attr(uname)
488
+ if text = update_attribute_hash[uname]
489
+ text.to_s
490
+ else
491
+ nil
492
+ end
493
+ end
494
+
495
+ end
496
+
497
+ end