Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 344
- Log:
Massive changeset which brings the old, ROOL customised Instiki
version up to date, but without any ROOL customisations in this
latest checked-in version (which is 0.19.1). This is deliberate,
so that it's easy to see the changes made for the ROOL version
in a subsequent changeset. The 'app/views/shared' directory is not
part of Instiki but is kept to maintain the change history with
updated ROOL customisations, some of which involve the same files
in that same directory.
- Author:
- rool
- Date:
- Sat Mar 19 19:52:13 +0000 2011
- Size:
- 17420 Bytes
1 | require 'strscan' |
2 | |
3 | module XHTML #:nodoc: |
4 | |
5 | class Conditions < Hash #:nodoc: |
6 | def initialize(hash) |
7 | super() |
8 | hash = { :content => hash } unless Hash === hash |
9 | hash = keys_to_symbols(hash) |
10 | hash.each do |k,v| |
11 | case k |
12 | when :tag, :content then |
13 | # keys are valid, and require no further processing |
14 | when :attributes then |
15 | hash[k] = keys_to_strings(v) |
16 | when :parent, :child, :ancestor, :descendant, :sibling, :before, |
17 | :after |
18 | hash[k] = Conditions.new(v) |
19 | when :children |
20 | hash[k] = v = keys_to_symbols(v) |
21 | v.each do |k,v2| |
22 | case k |
23 | when :count, :greater_than, :less_than |
24 | # keys are valid, and require no further processing |
25 | when :only |
26 | v[k] = Conditions.new(v2) |
27 | else |
28 | raise "illegal key #{k.inspect} => #{v2.inspect}" |
29 | end |
30 | end |
31 | else |
32 | raise "illegal key #{k.inspect} => #{v.inspect}" |
33 | end |
34 | end |
35 | update hash |
36 | end |
37 | |
38 | private |
39 | |
40 | def keys_to_strings(hash) |
41 | hash.keys.inject({}) do |h,k| |
42 | h[k.to_s] = hash[k] |
43 | h |
44 | end |
45 | end |
46 | |
47 | def keys_to_symbols(hash) |
48 | hash.keys.inject({}) do |h,k| |
49 | raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym) |
50 | h[k.to_sym] = hash[k] |
51 | h |
52 | end |
53 | end |
54 | end |
55 | |
56 | # The base class of all nodes, textual and otherwise, in an HTML document. |
57 | class Node #:nodoc: |
58 | # The array of children of this node. Not all nodes have children. |
59 | attr_reader :children |
60 | |
61 | # The parent node of this node. All nodes have a parent, except for the |
62 | # root node. |
63 | attr_reader :parent |
64 | |
65 | # The line number of the input where this node was begun |
66 | attr_reader :line |
67 | |
68 | # The byte position in the input where this node was begun |
69 | attr_reader :position |
70 | |
71 | # Create a new node as a child of the given parent. |
72 | def initialize(parent, line=0, pos=0) |
73 | @parent = parent |
74 | @children = [] |
75 | @line, @position = line, pos |
76 | end |
77 | |
78 | # Return a textual representation of the node. |
79 | def to_s |
80 | s = [] |
81 | @children.each { |child| s << child.to_s } |
82 | s.join |
83 | end |
84 | |
85 | # Return false (subclasses must override this to provide specific matching |
86 | # behavior.) +conditions+ may be of any type. |
87 | def match(conditions) |
88 | false |
89 | end |
90 | |
91 | # Search the children of this node for the first node for which #find |
92 | # returns non +nil+. Returns the result of the #find call that succeeded. |
93 | def find(conditions) |
94 | conditions = validate_conditions(conditions) |
95 | @children.each do |child| |
96 | node = child.find(conditions) |
97 | return node if node |
98 | end |
99 | nil |
100 | end |
101 | |
102 | # Search for all nodes that match the given conditions, and return them |
103 | # as an array. |
104 | def find_all(conditions) |
105 | conditions = validate_conditions(conditions) |
106 | |
107 | matches = [] |
108 | matches << self if match(conditions) |
109 | @children.each do |child| |
110 | matches.concat child.find_all(conditions) |
111 | end |
112 | matches |
113 | end |
114 | |
115 | # Returns +false+. Subclasses may override this if they define a kind of |
116 | # tag. |
117 | def tag? |
118 | false |
119 | end |
120 | |
121 | def validate_conditions(conditions) |
122 | Conditions === conditions ? conditions : Conditions.new(conditions) |
123 | end |
124 | |
125 | def ==(node) |
126 | return false unless self.class == node.class && children.size == node.children.size |
127 | |
128 | equivalent = true |
129 | |
130 | children.size.times do |i| |
131 | equivalent &&= children[i] == node.children[i] |
132 | end |
133 | |
134 | equivalent |
135 | end |
136 | |
137 | class <<self |
138 | def parse(parent, line, pos, content, strict=true) |
139 | if content !~ /^<\S/ |
140 | Text.new(parent, line, pos, content) |
141 | else |
142 | scanner = StringScanner.new(content) |
143 | |
144 | unless scanner.skip(/</) |
145 | if strict |
146 | raise "expected <" |
147 | else |
148 | return Text.new(parent, line, pos, content) |
149 | end |
150 | end |
151 | |
152 | if scanner.skip(/!\[CDATA\[/) |
153 | unless scanner.skip_until(/\]\]>/) |
154 | if strict |
155 | raise "expected ]]> (got #{scanner.rest.inspect} for #{content})" |
156 | else |
157 | scanner.skip_until(/\Z/) |
158 | end |
159 | end |
160 | |
161 | return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, '')) |
162 | end |
163 | |
164 | closing = ( scanner.scan(/\//) ? :close : nil ) |
165 | return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/) |
166 | |
167 | unless closing |
168 | scanner.skip(/\s*/) |
169 | attributes = {} |
170 | while attr = scanner.scan(/[-\w:]+/) |
171 | value = true |
172 | if scanner.scan(/\s*=\s*/) |
173 | if delim = scanner.scan(/['"]/) |
174 | v = [] |
175 | while text = scanner.scan(/[^#{delim}\\]+|./) |
176 | case text |
177 | when "\\" then |
178 | v << text |
179 | v << scanner.getch |
180 | when delim |
181 | break |
182 | else v << text |
183 | end |
184 | end |
185 | value = v.join |
186 | else |
187 | value = scanner.scan(/[^\s>\/]+/) |
188 | end |
189 | end |
190 | attributes[attr] = value |
191 | scanner.skip(/\s*/) |
192 | end |
193 | |
194 | closing = ( scanner.scan(/\//) ? :self : nil ) |
195 | end |
196 | |
197 | unless scanner.scan(/\s*>/) |
198 | if strict |
199 | raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" |
200 | else |
201 | # throw away all text until we find what we're looking for |
202 | scanner.skip_until(/>/) or scanner.terminate |
203 | end |
204 | end |
205 | |
206 | Tag.new(parent, line, pos, name, attributes, closing) |
207 | end |
208 | end |
209 | end |
210 | end |
211 | |
212 | # A node that represents text, rather than markup. |
213 | class Text < Node #:nodoc: |
214 | |
215 | attr_reader :content |
216 | |
217 | # Creates a new text node as a child of the given parent, with the given |
218 | # content. |
219 | def initialize(parent, line, pos, content) |
220 | super(parent, line, pos) |
221 | @content = content |
222 | end |
223 | |
224 | # Returns the content of this node. |
225 | def to_s |
226 | @content |
227 | end |
228 | |
229 | # Returns +self+ if this node meets the given conditions. Text nodes support |
230 | # conditions of the following kinds: |
231 | # |
232 | # * if +conditions+ is a string, it must be a substring of the node's |
233 | # content |
234 | # * if +conditions+ is a regular expression, it must match the node's |
235 | # content |
236 | # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that |
237 | # is either a string or a regexp, and which is interpreted as described |
238 | # above. |
239 | def find(conditions) |
240 | match(conditions) && self |
241 | end |
242 | |
243 | # Returns non-+nil+ if this node meets the given conditions, or +nil+ |
244 | # otherwise. See the discussion of #find for the valid conditions. |
245 | def match(conditions) |
246 | case conditions |
247 | when String |
248 | @content == conditions |
249 | when Regexp |
250 | @content =~ conditions |
251 | when Hash |
252 | conditions = validate_conditions(conditions) |
253 | |
254 | # Text nodes only have :content, :parent, :ancestor |
255 | unless (conditions.keys - [:content, :parent, :ancestor]).empty? |
256 | return false |
257 | end |
258 | |
259 | match(conditions[:content]) |
260 | else |
261 | nil |
262 | end |
263 | end |
264 | |
265 | def ==(node) |
266 | return false unless super |
267 | content == node.content |
268 | end |
269 | end |
270 | |
271 | # A CDATA node is simply a text node with a specialized way of displaying |
272 | # itself. |
273 | class CDATA < Text #:nodoc: |
274 | def to_s |
275 | "<![CDATA[#{super}]]>" |
276 | end |
277 | end |
278 | |
279 | # A Tag is any node that represents markup. It may be an opening tag, a |
280 | # closing tag, or a self-closing tag. It has a name, and may have a hash of |
281 | # attributes. |
282 | class Tag < Node #:nodoc: |
283 | |
284 | # Either +nil+, <tt>:close</tt>, or <tt>:self</tt> |
285 | attr_reader :closing |
286 | |
287 | # Either +nil+, or a hash of attributes for this node. |
288 | attr_reader :attributes |
289 | |
290 | # The name of this tag. |
291 | attr_reader :name |
292 | |
293 | # Create a new node as a child of the given parent, using the given content |
294 | # to describe the node. It will be parsed and the node name, attributes and |
295 | # closing status extracted. |
296 | def initialize(parent, line, pos, name, attributes, closing) |
297 | super(parent, line, pos) |
298 | @name = name |
299 | @attributes = attributes |
300 | @closing = closing |
301 | end |
302 | |
303 | # A convenience for obtaining an attribute of the node. Returns +nil+ if |
304 | # the node has no attributes. |
305 | def [](attr) |
306 | @attributes ? @attributes[attr] : nil |
307 | end |
308 | |
309 | # Returns non-+nil+ if this tag can contain child nodes. |
310 | def childless?(xml = false) |
311 | return false if xml && @closing.nil? |
312 | # !@closing.nil? || |
313 | @name =~ /^(img|br|hr|link|meta|area|base|basefont| |
314 | col|frame|input|isindex|param)$/ox |
315 | end |
316 | |
317 | # Returns a textual representation of the node |
318 | def to_s |
319 | if @closing == :close |
320 | "</#{@name}>" unless self.childless? |
321 | else |
322 | s = ["<#{@name}"] |
323 | @attributes.sort.each do |k,v| |
324 | s << " #{k}" |
325 | s << "='#{v}'" if String === v |
326 | end |
327 | s << "/" if (@children.empty? && @closing == :self) or self.childless? |
328 | s << ">" |
329 | @children.each { |child| s << child.to_s } |
330 | s << "</#{@name}>" unless @closing == :self or self.childless? or @children.empty? |
331 | s.join |
332 | end |
333 | end |
334 | |
335 | # If either the node or any of its children meet the given conditions, the |
336 | # matching node is returned. Otherwise, +nil+ is returned. (See the |
337 | # description of the valid conditions in the +match+ method.) |
338 | def find(conditions) |
339 | match(conditions) && self || super |
340 | end |
341 | |
342 | # Returns +true+, indicating that this node represents an HTML tag. |
343 | def tag? |
344 | true |
345 | end |
346 | |
347 | # Returns +true+ if the node meets any of the given conditions. The |
348 | # +conditions+ parameter must be a hash of any of the following keys |
349 | # (all are optional): |
350 | # |
351 | # * <tt>:tag</tt>: the node name must match the corresponding value |
352 | # * <tt>:attributes</tt>: a hash. The node's values must match the |
353 | # corresponding values in the hash. |
354 | # * <tt>:parent</tt>: a hash. The node's parent must match the |
355 | # corresponding hash. |
356 | # * <tt>:child</tt>: a hash. At least one of the node's immediate children |
357 | # must meet the criteria described by the hash. |
358 | # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must |
359 | # meet the criteria described by the hash. |
360 | # * <tt>:descendant</tt>: a hash. At least one of the node's descendants |
361 | # must meet the criteria described by the hash. |
362 | # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must |
363 | # meet the criteria described by the hash. |
364 | # * <tt>:after</tt>: a hash. The node must be after any sibling meeting |
365 | # the criteria described by the hash, and at least one sibling must match. |
366 | # * <tt>:before</tt>: a hash. The node must be before any sibling meeting |
367 | # the criteria described by the hash, and at least one sibling must match. |
368 | # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the |
369 | # keys: |
370 | # ** <tt>:count</tt>: either a number or a range which must equal (or |
371 | # include) the number of children that match. |
372 | # ** <tt>:less_than</tt>: the number of matching children must be less than |
373 | # this number. |
374 | # ** <tt>:greater_than</tt>: the number of matching children must be |
375 | # greater than this number. |
376 | # ** <tt>:only</tt>: another hash consisting of the keys to use |
377 | # to match on the children, and only matching children will be |
378 | # counted. |
379 | # |
380 | # Conditions are matched using the following algorithm: |
381 | # |
382 | # * if the condition is a string, it must be a substring of the value. |
383 | # * if the condition is a regexp, it must match the value. |
384 | # * if the condition is a number, the value must match number.to_s. |
385 | # * if the condition is +true+, the value must not be +nil+. |
386 | # * if the condition is +false+ or +nil+, the value must be +nil+. |
387 | # |
388 | # Usage: |
389 | # |
390 | # # test if the node is a "span" tag |
391 | # node.match :tag => "span" |
392 | # |
393 | # # test if the node's parent is a "div" |
394 | # node.match :parent => { :tag => "div" } |
395 | # |
396 | # # test if any of the node's ancestors are "table" tags |
397 | # node.match :ancestor => { :tag => "table" } |
398 | # |
399 | # # test if any of the node's immediate children are "em" tags |
400 | # node.match :child => { :tag => "em" } |
401 | # |
402 | # # test if any of the node's descendants are "strong" tags |
403 | # node.match :descendant => { :tag => "strong" } |
404 | # |
405 | # # test if the node has between 2 and 4 span tags as immediate children |
406 | # node.match :children => { :count => 2..4, :only => { :tag => "span" } } |
407 | # |
408 | # # get funky: test to see if the node is a "div", has a "ul" ancestor |
409 | # # and an "li" parent (with "class" = "enum"), and whether or not it has |
410 | # # a "span" descendant that contains # text matching /hello world/: |
411 | # node.match :tag => "div", |
412 | # :ancestor => { :tag => "ul" }, |
413 | # :parent => { :tag => "li", |
414 | # :attributes => { :class => "enum" } }, |
415 | # :descendant => { :tag => "span", |
416 | # :child => /hello world/ } |
417 | def match(conditions) |
418 | conditions = validate_conditions(conditions) |
419 | # check content of child nodes |
420 | if conditions[:content] |
421 | if children.empty? |
422 | return false unless match_condition("", conditions[:content]) |
423 | else |
424 | return false unless children.find { |child| child.match(conditions[:content]) } |
425 | end |
426 | end |
427 | |
428 | # test the name |
429 | return false unless match_condition(@name, conditions[:tag]) if conditions[:tag] |
430 | |
431 | # test attributes |
432 | (conditions[:attributes] || {}).each do |key, value| |
433 | return false unless match_condition(self[key], value) |
434 | end |
435 | |
436 | # test parent |
437 | return false unless parent.match(conditions[:parent]) if conditions[:parent] |
438 | |
439 | # test children |
440 | return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child] |
441 | |
442 | # test ancestors |
443 | if conditions[:ancestor] |
444 | return false unless catch :found do |
445 | p = self |
446 | throw :found, true if p.match(conditions[:ancestor]) while p = p.parent |
447 | end |
448 | end |
449 | |
450 | # test descendants |
451 | if conditions[:descendant] |
452 | return false unless children.find do |child| |
453 | # test the child |
454 | child.match(conditions[:descendant]) || |
455 | # test the child's descendants |
456 | child.match(:descendant => conditions[:descendant]) |
457 | end |
458 | end |
459 | |
460 | # count children |
461 | if opts = conditions[:children] |
462 | matches = children.select do |c| |
463 | (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) |
464 | end |
465 | |
466 | matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] |
467 | opts.each do |key, value| |
468 | next if key == :only |
469 | case key |
470 | when :count |
471 | if Integer === value |
472 | return false if matches.length != value |
473 | else |
474 | return false unless value.include?(matches.length) |
475 | end |
476 | when :less_than |
477 | return false unless matches.length < value |
478 | when :greater_than |
479 | return false unless matches.length > value |
480 | else raise "unknown count condition #{key}" |
481 | end |
482 | end |
483 | end |
484 | |
485 | # test siblings |
486 | if conditions[:sibling] || conditions[:before] || conditions[:after] |
487 | siblings = parent ? parent.children : [] |
488 | self_index = siblings.index(self) |
489 | |
490 | if conditions[:sibling] |
491 | return false unless siblings.detect do |s| |
492 | s != self && s.match(conditions[:sibling]) |
493 | end |
494 | end |
495 | |
496 | if conditions[:before] |
497 | return false unless siblings[self_index+1..-1].detect do |s| |
498 | s != self && s.match(conditions[:before]) |
499 | end |
500 | end |
501 | |
502 | if conditions[:after] |
503 | return false unless siblings[0,self_index].detect do |s| |
504 | s != self && s.match(conditions[:after]) |
505 | end |
506 | end |
507 | end |
508 | |
509 | true |
510 | end |
511 | |
512 | def ==(node) |
513 | return false unless super |
514 | return false unless closing == node.closing && self.name == node.name |
515 | attributes == node.attributes |
516 | end |
517 | |
518 | private |
519 | # Match the given value to the given condition. |
520 | def match_condition(value, condition) |
521 | case condition |
522 | when String |
523 | value && value == condition |
524 | when Regexp |
525 | value && value.match(condition) |
526 | when Numeric |
527 | value == condition.to_s |
528 | when true |
529 | !value.nil? |
530 | when false, nil |
531 | value.nil? |
532 | else |
533 | false |
534 | end |
535 | end |
536 | end |
537 | end |
538 |