raakt 0.1

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 (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