raakt 0.4 → 0.5

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.
@@ -1,495 +0,0 @@
1
- # :title: Ruby Accessibility Analysis Kit
2
- # =Ruby Accessibility Analysis Kit
3
- #
4
- # See README for a full explanation of this library.
5
-
6
- module Raakt
7
- require 'rubyful_soup'
8
-
9
- MESSAGES = {
10
- "missingtitle" => "The title element is missing. Provide a descriptive title for your document.",
11
- "emptytitle" => "The title element is empty. Provide a descriptive title for your document.",
12
- "missingalt" => "Missing alt attribute for image (with src '%s').",
13
- "missingheading" => "Missing first level heading (h1). Provide at least one first level heading describing document content.",
14
- "wronghstructure" => "Document heading structure is wrong.",
15
- "firsthnoth1" => "The first heading is not h1.",
16
- "hasnestedtables" => "You have one or more nested tables.",
17
- "missingsemantics"=> "You have used <font>, <b> or <i> for visual formatting. Use CSS instead.",
18
- "hasflicker" => "You have used <blink> or <marquee>. These may create accessibility issues and should be avoided.",
19
- "missinglanginfo" => "Document language information is missing. Use the lang attribute on the html element.",
20
- "missingth" => "Missing table headings (th) for table #%s.",
21
- "ambiguouslinktext" => "One or more links have the same link text ('%s'). Make sure each link is unambiguous.",
22
- "fieldmissinglabel" => "A field (with id/name '%s') is missing a corresponding label element. Make sure a label exists for all visible fields.",
23
- "missingframetitle" => "Missing title attribute for frame with url %s",
24
- "hasmetarefresh" => "Client side redirect (meta refresh) detected. Use server side redirection instead."
25
- }
26
-
27
- VERSION = "0.31"
28
-
29
- class ErrorMessage
30
-
31
- attr_reader :eid, :text, :note
32
-
33
- def initialize(eid, note=nil)
34
- @eid = eid
35
- if note
36
- @text = MESSAGES[eid].sub(/%s/, note)
37
- else
38
- @text = MESSAGES[eid]
39
- end
40
- @note = note
41
- end
42
-
43
- def to_s
44
- @eid + ": " + @text
45
- end
46
- end
47
-
48
-
49
-
50
-
51
-
52
- class Test
53
-
54
- attr_accessor :soup, :html, :user_agent
55
-
56
- def initialize(html=nil)
57
- @html = html
58
- @soup = BeautifulSoup.new(@html) if html
59
- @user_agent = "Mozilla/5.0 (RAAKT v#{VERSION}; http://raakt.rubyforge.org; The Ruby Accessibility Analysis Kit)"
60
- end
61
-
62
- def feed(html)
63
- @html = html || ""
64
- if @html.length > 0
65
- @soup = BeautifulSoup.new(@html)
66
- else
67
- raise "You called feed with no data. There is nothing to check."
68
- end
69
- end
70
-
71
-
72
-
73
- def feedurl(url)
74
- if url.length == 0
75
- raise "You called feedurl with a blank url. There is nothing to check."
76
- end
77
-
78
- #Clean the url and make sure protocol and trailing slash is available
79
- url = "http://" + url unless url[0..3] == "http"
80
-
81
- require 'open-uri'
82
-
83
- open(url, "User-Agent" => @user_agent) { |f|
84
- @html = f.read || ""
85
- }
86
-
87
- if @html.length == 0
88
- raise "Could not fetch html from the url #{url}. There is nothing to check."
89
- else
90
- @soup = BeautifulSoup.new(@html)
91
- end
92
-
93
- end
94
-
95
-
96
-
97
- def all
98
- #Call all check methods
99
- messages = []
100
-
101
- self.methods.each do |method|
102
- if method[0..5] == "check_"
103
- messages += self.send(method)
104
- end
105
- end
106
-
107
- return messages
108
- end
109
-
110
-
111
- def check_images
112
- #soup = BeautifulSoup.new(html)
113
- images = @soup.find_all("img")
114
- messages = []
115
-
116
- for image in images:
117
- if image["alt"] == nil:
118
- img_src = image["src"] || ""
119
- messages << ErrorMessage.new("missingalt", img_src)
120
- end
121
- end
122
-
123
- return messages
124
- end
125
-
126
-
127
- def check_title
128
- title = @soup.find("title")
129
- messages = []
130
-
131
- if title
132
- titletext = normalize_text(title.string)
133
- if titletext.length == 0
134
- messages << ErrorMessage.new("emptytitle")
135
- end
136
- else
137
- messages << ErrorMessage.new("missingtitle")
138
- end
139
-
140
- return messages
141
- end
142
-
143
-
144
- def check_has_heading
145
- messages = []
146
-
147
- if @soup.find_all("h1").length == 0
148
- messages << ErrorMessage.new("missingheading")
149
- end
150
-
151
- return messages
152
- end
153
-
154
-
155
- def headings
156
- headings = []
157
- headings.push(@soup.find_all("h1")) if @soup.find_all("h1").length > 0
158
- headings.push(@soup.find_all("h2")) if @soup.find_all("h2").length > 0
159
- headings.push(@soup.find_all("h3")) if @soup.find_all("h3").length > 0
160
- headings.push(@soup.find_all("h4")) if @soup.find_all("h4").length > 0
161
- headings.push(@soup.find_all("h5")) if @soup.find_all("h5").length > 0
162
- headings.push(@soup.find_all("h6")) if @soup.find_all("h6").length > 0
163
-
164
- return headings.flatten
165
- end
166
-
167
-
168
- def level(heading)
169
- Integer(heading[1,1])
170
- end
171
-
172
-
173
- def check_document_structure
174
- messages = []
175
- currentitem = 0
176
- docheadings = headings
177
-
178
- for heading in docheadings
179
- if currentitem == 0
180
- if level(heading.name) != 1
181
- messages << ErrorMessage.new("firsthnoth1", "h" + heading.name[1,1])
182
- end
183
- else
184
- if level(heading.name) - level(docheadings[currentitem - 1].name) > 1
185
- messages << ErrorMessage.new("wronghstructure")
186
- break
187
- end
188
- end
189
-
190
- currentitem += 1
191
-
192
- end
193
-
194
- return messages
195
- end
196
-
197
-
198
- def check_for_nested_tables
199
-
200
- messages = []
201
- tables = @soup.find_all("table")
202
-
203
- for table in tables
204
- if table.find_all("table").length > 0
205
- messages << ErrorMessage.new("hasnestedtables")
206
- break
207
- end
208
- end
209
-
210
- return messages
211
- end
212
-
213
-
214
- def check_tables
215
-
216
- messages = []
217
- tables = @soup.find_all("table")
218
- hasth = false
219
- currenttable = 1
220
-
221
- for table in tables
222
- if table.thead
223
- if table.thead.tr
224
- if table.thead.tr.th
225
- hasth = true
226
- end
227
- end
228
- end
229
-
230
- if table.tr
231
- if table.tr.th
232
- hasth = true
233
- end
234
- end
235
-
236
- unless hasth
237
- messages << ErrorMessage.new("missingth", currenttable.to_s)
238
- end
239
-
240
- currenttable += 1
241
- end
242
-
243
- return messages
244
- end
245
-
246
-
247
- def check_for_formatting_elements
248
-
249
- messages = []
250
- formatting_items = @soup.find_all(["font", "b", "i"])
251
- flicker_items = @soup.find_all(["blink", "marquee"])
252
-
253
- if formatting_items.length > 0
254
- messages << ErrorMessage.new("missingsemantics")
255
- end
256
-
257
- if flicker_items.length > 0
258
- messages << ErrorMessage.new("hasflicker")
259
- end
260
-
261
- return messages
262
- end
263
-
264
-
265
- def check_for_language_info
266
- messages = []
267
-
268
- htmlelement = @soup.find("html")
269
-
270
- lang = langinfo(htmlelement) || ""
271
-
272
- unless lang.length > 1
273
- messages << ErrorMessage.new("missinglanginfo")
274
- end
275
-
276
- return messages
277
- end
278
-
279
-
280
- def check_link_text
281
- messages = []
282
- links = get_links
283
- linktexts = links.collect { |el| el[3] }
284
-
285
- for link_a in links
286
- #compare to other links in collection
287
- for link_b in links
288
- if link_a[0] != link_b[0]
289
- if is_ambiguous_link(link_a, link_b)
290
- #add message if not added already for link text
291
- unless find_errormsg_with_text(messages, link_a[3])
292
- messages << ErrorMessage.new("ambiguouslinktext", link_a[3])
293
- end
294
- end
295
- end
296
- end
297
- end
298
-
299
- return messages
300
- end
301
-
302
-
303
- def check_form
304
- messages = []
305
- labels = get_labels
306
- fields = get_editable_fields
307
-
308
- #make sure all fields have associated labels
309
-
310
- label_for_ids = []
311
- for label in labels
312
- if label["for"]
313
- label_for_ids << label["for"]
314
- end
315
- end
316
-
317
- field_id = nil
318
-
319
- for field in fields
320
- field_id = (field["id"] || "")
321
- field_identifier = (field["id"] || field["name"] || "unknown")
322
- if not label_for_ids.include?(field_id)
323
- messages << ErrorMessage.new("fieldmissinglabel", field_identifier)
324
- end
325
- end
326
-
327
- return messages
328
- end
329
-
330
-
331
- def check_frames
332
- #Verify frame titles
333
-
334
- messages = []
335
- if is_frameset
336
- frames = @soup.find_all("frame")
337
- frame_title = ""
338
-
339
- for frame in frames
340
- frame_title = frame["title"] || ""
341
- if normalize_text(frame_title).length == 0
342
- messages << ErrorMessage.new("missingframetitle", frame["src"])
343
- end
344
- end
345
- end
346
-
347
- return messages
348
- end
349
-
350
-
351
- def check_refresh
352
-
353
- messages = []
354
- meta_elements = @soup.find_all("meta")
355
-
356
- for element in meta_elements
357
- if element["http-equiv"] == "refresh"
358
- messages << ErrorMessage.new("hasmetarefresh")
359
- end
360
- end
361
-
362
- return messages
363
- end
364
-
365
-
366
- #Utility methods
367
-
368
- def is_ambiguous_link(link_a, link_b)
369
- #Link A and B are ambiguous if:
370
- #1. The url differs
371
- #2. The link text is identical
372
- #3. The title text is identical (if present)
373
- if link_a[1] != link_b[1] and
374
- normalize_text(link_a[2]) == normalize_text(link_b[2]) and
375
- normalize_text(link_a[3]) == normalize_text(link_b[3]) then
376
- return true
377
- end
378
-
379
- return false
380
- end
381
-
382
-
383
- def find_errormsg_with_text(messages, text)
384
- for errormessage in messages
385
- if errormessage.note == text
386
- return errormessage
387
- end
388
- end
389
-
390
- return nil
391
- end
392
-
393
-
394
- def get_links
395
- linkelements = @soup.find_all("a")
396
- links = []
397
- currentlink = 0
398
-
399
- for element in linkelements
400
- title = normalize_text((element['title'] || "").strip)
401
- linktext = normalize_text((elements_to_text(element) || "").strip)
402
- url = element['href']
403
- links << [currentlink, url, title, linktext]
404
- currentlink += 1
405
- end
406
-
407
- return links
408
- end
409
-
410
-
411
- def langinfo(element)
412
- langval = ""
413
-
414
- if element.class.to_s == 'Tag'
415
- if element['lang']
416
- langval = element['lang']
417
- end
418
- else
419
- return nil
420
- end
421
-
422
- return langval
423
- end
424
-
425
-
426
- def img_to_text(imgtag)
427
- return (imgtag['alt'] || "")
428
- end
429
-
430
-
431
- def elements_to_text(element)
432
- retval = ""
433
-
434
- for el in element.contents
435
- if el.class.to_s == 'NavigableString'
436
- retval += el
437
- else
438
- if el.name == "img"
439
- retval += img_to_text(el)
440
- else
441
- retval += elements_to_text(el)
442
- end
443
- end
444
- end
445
-
446
- return retval
447
- end
448
-
449
-
450
- def normalize_text(text)
451
- text = (text || "")
452
- retval = text.gsub(/&nbsp;/, " ")
453
- retval = retval.gsub(/&#160;/, " ")
454
- retval = retval.gsub(/\n/, "")
455
- retval = retval.gsub(/\r/, "")
456
- retval = retval.gsub(/\t/, "")
457
- while / /.match(retval) do
458
- retval = retval.gsub(/ /, " ")
459
- end
460
-
461
- retval = retval.strip
462
-
463
- return retval
464
- end
465
-
466
-
467
- def get_labels
468
- return @soup.find_all("label")
469
- end
470
-
471
-
472
- def get_editable_fields
473
- allfields = @soup.find_all(["textarea", "select", "input"])
474
- fields = []
475
- field_type = ""
476
-
477
- for field in allfields do
478
- field_type = field["type"] || ""
479
- unless ["button", "submit", "hidden", "image"].include?(field_type)
480
- fields << field
481
- end
482
-
483
- end
484
-
485
- return fields
486
- end
487
-
488
-
489
- def is_frameset
490
- return (@soup.find("frameset") != nil)
491
- end
492
-
493
- end
494
-
495
- end