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.
- data/lib/raakt.rb +454 -0
- data/tests/empty.htm +1 -0
- data/tests/emptytitledoc.htm +8 -0
- data/tests/fielddoc1.htm +2 -0
- data/tests/fielddoc2.htm +11 -0
- data/tests/fielddoc3.htm +14 -0
- data/tests/flickerdoc1.htm +0 -0
- data/tests/framedoc1.htm +22 -0
- data/tests/framedoc2.htm +8 -0
- data/tests/full_google.htm +17 -0
- data/tests/headingsdoc1.htm +17 -0
- data/tests/headingsdoc2.htm +14 -0
- data/tests/headingsdoc3.htm +6 -0
- data/tests/headingsdoc4.htm +9 -0
- data/tests/headingsdoc5.htm +9 -0
- data/tests/headingsdoc6.htm +6 -0
- data/tests/headingsdoc7.htm +8 -0
- data/tests/headingsdoc8.htm +12 -0
- data/tests/headingsdoc9.htm +20 -0
- data/tests/imagedoc1.htm +8 -0
- data/tests/imagedoc2.htm +1 -0
- data/tests/imagedoc3.htm +11 -0
- data/tests/imagedoc4.htm +7 -0
- data/tests/invalidelements1.htm +18 -0
- data/tests/invalidhtmldoc1.htm +10 -0
- data/tests/invalidhtmldoc2.htm +20 -0
- data/tests/invalidxhtmldoc1.htm +17 -0
- data/tests/linkdoc1.htm +18 -0
- data/tests/linkdoc2.htm +12 -0
- data/tests/linkdoc3.htm +16 -0
- data/tests/linkdoc4.htm +10 -0
- data/tests/metarefreshdoc1.htm +10 -0
- data/tests/metarefreshdoc2.htm +14 -0
- data/tests/metarefreshdoc3.htm +10 -0
- data/tests/nestedcomment.htm +7 -0
- data/tests/newlinetext.txt +3 -0
- data/tests/raakt_test.rb +224 -0
- data/tests/scriptdoc1.htm +15 -0
- data/tests/scriptdoc2.htm +10 -0
- data/tests/tabledoc1.htm +5 -0
- data/tests/tabledoc2.htm +9 -0
- data/tests/tabledoc3.htm +6 -0
- data/tests/tabledoc4.htm +17 -0
- data/tests/tabledoc5.htm +11 -0
- data/tests/tabledoc6.htm +11 -0
- data/tests/tablelayoutdoc.htm +16 -0
- data/tests/test_helper.rb +21 -0
- data/tests/xhtmldoc1.htm +14 -0
- metadata +100 -0
data/lib/raakt.rb
ADDED
@@ -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(/ /, " ")
|
409
|
+
retval = retval.gsub(/ /, " ")
|
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
|