raakt 0.1

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