hatemile 2.0

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +46 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE +202 -0
  5. data/Rakefile +64 -0
  6. data/hatemile.gemspec +37 -0
  7. data/lib/hatemile/accessible_association.rb +62 -0
  8. data/lib/hatemile/accessible_css.rb +43 -0
  9. data/lib/hatemile/accessible_display.rb +178 -0
  10. data/lib/hatemile/accessible_event.rb +95 -0
  11. data/lib/hatemile/accessible_form.rb +101 -0
  12. data/lib/hatemile/accessible_navigation.rb +82 -0
  13. data/lib/hatemile/helper.rb +58 -0
  14. data/lib/hatemile/implementation/accessible_association_implementation.rb +346 -0
  15. data/lib/hatemile/implementation/accessible_css_implementation.rb +772 -0
  16. data/lib/hatemile/implementation/accessible_display_implementation.rb +1362 -0
  17. data/lib/hatemile/implementation/accessible_event_implementation.rb +278 -0
  18. data/lib/hatemile/implementation/accessible_form_implementation.rb +386 -0
  19. data/lib/hatemile/implementation/accessible_navigation_implementation.rb +561 -0
  20. data/lib/hatemile/util/common_functions.rb +106 -0
  21. data/lib/hatemile/util/configure.rb +92 -0
  22. data/lib/hatemile/util/css/rcp/rcp_declaration.rb +77 -0
  23. data/lib/hatemile/util/css/rcp/rcp_parser.rb +115 -0
  24. data/lib/hatemile/util/css/rcp/rcp_rule.rb +86 -0
  25. data/lib/hatemile/util/css/style_sheet_declaration.rb +59 -0
  26. data/lib/hatemile/util/css/style_sheet_parser.rb +43 -0
  27. data/lib/hatemile/util/css/style_sheet_rule.rb +73 -0
  28. data/lib/hatemile/util/html/html_dom_element.rb +234 -0
  29. data/lib/hatemile/util/html/html_dom_node.rb +131 -0
  30. data/lib/hatemile/util/html/html_dom_parser.rb +150 -0
  31. data/lib/hatemile/util/html/html_dom_text_node.rb +43 -0
  32. data/lib/hatemile/util/html/nokogiri/nokogiri_html_dom_element.rb +302 -0
  33. data/lib/hatemile/util/html/nokogiri/nokogiri_html_dom_node.rb +112 -0
  34. data/lib/hatemile/util/html/nokogiri/nokogiri_html_dom_parser.rb +208 -0
  35. data/lib/hatemile/util/html/nokogiri/nokogiri_html_dom_text_node.rb +83 -0
  36. data/lib/hatemile/util/id_generator.rb +53 -0
  37. data/lib/js/common.js +98 -0
  38. data/lib/js/eventlistener.js +36 -0
  39. data/lib/js/include.js +292 -0
  40. data/lib/js/scriptlist_validation_fields.js +13 -0
  41. data/lib/js/validation.js +205 -0
  42. data/lib/locale/en-US.yml +388 -0
  43. data/lib/locale/pt-BR.yml +389 -0
  44. data/lib/skippers.xml +6 -0
  45. data/lib/symbols.xml +40 -0
  46. data/test/locale/en-US.yml +5 -0
  47. data/test/locale/pt-BR.yml +4 -0
  48. data/test/test_accessible_association_implementation.rb +258 -0
  49. data/test/test_accessible_css_implementation.rb +518 -0
  50. data/test/test_accessible_display_implementation.rb +873 -0
  51. data/test/test_accessible_form_implementation.rb +283 -0
  52. data/test/test_accessible_navigation_implementation.rb +228 -0
  53. data/test/test_common_functions.rb +128 -0
  54. data/test/test_configure.rb +73 -0
  55. data/test/test_nokogiri_html_dom_element.rb +586 -0
  56. data/test/test_nokogiri_html_dom_parser.rb +363 -0
  57. data/test/test_nokogiri_html_dom_text_node.rb +225 -0
  58. data/test/test_rcp_declaration.rb +103 -0
  59. data/test/test_rcp_parser.rb +86 -0
  60. data/test/test_rcp_rule.rb +89 -0
  61. metadata +199 -0
@@ -0,0 +1,58 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ ##
14
+ # The Hatemile module contains the interfaces with the acessibility solutions.
15
+ module Hatemile
16
+ ##
17
+ # Helper methods of HaTeMiLe for Ruby.
18
+ module Helper
19
+ ##
20
+ # Error class for nil arguments.
21
+ class NotNilError < StandardError; end
22
+
23
+ ##
24
+ # Checks that the specified objects references is not nil and throws a
25
+ # TypeError if it is.
26
+ #
27
+ # @param values [Array<Object>] The objects.
28
+ # @return [void]
29
+ def self.require_not_nil(*values)
30
+ values.each do |value|
31
+ if value.nil?
32
+ raise NotNilError.new('The value of parameter not be nil.')
33
+ end
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Checks that the specified object reference is instance of classes and
39
+ # TypeError
40
+ #
41
+ # @param value [Object] The object.
42
+ # @param classes [Array<Class>] The classes.
43
+ # @return [void]
44
+ def self.require_valid_type(value, *classes)
45
+ return if value.nil?
46
+
47
+ valid = false
48
+ classes.each do |auxiliar_class|
49
+ if value.is_a?(auxiliar_class)
50
+ valid = true
51
+ break
52
+ end
53
+ end
54
+
55
+ raise TypeError.new('Wrong type of argument.') unless valid
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,346 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ require File.join(
14
+ File.dirname(File.dirname(__FILE__)),
15
+ 'accessible_association'
16
+ )
17
+ require File.join(File.dirname(File.dirname(__FILE__)), 'helper')
18
+ require File.join(
19
+ File.dirname(File.dirname(__FILE__)),
20
+ 'util',
21
+ 'common_functions'
22
+ )
23
+ require File.join(File.dirname(File.dirname(__FILE__)), 'util', 'id_generator')
24
+ require File.join(
25
+ File.dirname(File.dirname(__FILE__)),
26
+ 'util',
27
+ 'html',
28
+ 'html_dom_parser'
29
+ )
30
+
31
+ ##
32
+ # The Hatemile module contains the interfaces with the acessibility solutions.
33
+ module Hatemile
34
+ ##
35
+ # The Hatemile::Implementation module contains the official implementation of
36
+ # interfaces solutions.
37
+ module Implementation
38
+ ##
39
+ # The AccessibleAssociationImplementation class is official implementation
40
+ # of AccessibleAssociation.
41
+ class AccessibleAssociationImplementation < AccessibleAssociation
42
+ public_class_method :new
43
+
44
+ protected
45
+
46
+ ##
47
+ # Returns a list that represents the table.
48
+ #
49
+ # @param part [Hatemile::Util::Html::HTMLDOMElement] The table header,
50
+ # table footer or table body.
51
+ # @return [Array<Array<Hatemile::Util::Html::HTMLDOMElement>>] The list
52
+ # that represents the table.
53
+ def get_model_table(part)
54
+ rows = @parser.find(part).find_children('tr').list_results
55
+ table = []
56
+ rows.each do |row|
57
+ table.push(
58
+ get_model_row(@parser.find(row).find_children('td,th').list_results)
59
+ )
60
+ end
61
+ get_valid_model_table(table)
62
+ end
63
+
64
+ ##
65
+ # Returns a list that represents the table with the rowspans.
66
+ #
67
+ # @param table [Array<Array<Hatemile::Util::Html::HTMLDOMElement>>] The
68
+ # list that represents the table without the rowspans.
69
+ # @return [Array<Array<Hatemile::Util::Html::HTMLDOMElement>>] The list
70
+ # that represents the table with the rowspans.
71
+ def get_valid_model_table(table)
72
+ new_table = []
73
+ unless table.empty?
74
+ length_table = table.size
75
+ (0..length_table - 1).each do |row_index|
76
+ cells_added = 0
77
+ original_row = [].concat(table[row_index])
78
+ new_table[row_index] = [] if new_table.size <= row_index
79
+ length_row = original_row.size
80
+ (0..length_row - 1).each do |cell_index|
81
+ cell = original_row[cell_index]
82
+ new_cell_index = cell_index + cells_added
83
+ new_row = new_table[row_index]
84
+ until new_row[new_cell_index].nil?
85
+ cells_added += 1
86
+ new_cell_index = cell_index + cells_added
87
+ end
88
+ new_row[new_cell_index] = cell
89
+
90
+ next unless cell.has_attribute?('rowspan')
91
+
92
+ rowspan = cell.get_attribute('rowspan').to_i
93
+
94
+ next unless rowspan > 1
95
+
96
+ (1..rowspan - 1).each do |rowspan_index|
97
+ new_row_index = row_index + rowspan_index
98
+ new_table[new_row_index] = [] if new_table[new_row_index].nil?
99
+ new_table[new_row_index][new_cell_index] = cell
100
+ end
101
+ end
102
+ end
103
+ end
104
+ new_table
105
+ end
106
+
107
+ ##
108
+ # Returns a list that represents the line of table with the colspans.
109
+ #
110
+ # @param row [Array<Hatemile::Util::Html::HTMLDOMElement>] The list that
111
+ # represents the line of table without the colspans.
112
+ # @return [Array<Hatemile::Util::Html::HTMLDOMElement>] The list that
113
+ # represents the line of table with the colspans.
114
+ def get_model_row(row)
115
+ new_row = [].concat(row)
116
+ size = row.size
117
+ (0..size - 1).each do |i|
118
+ cell = row[i]
119
+
120
+ next unless cell.has_attribute?('colspan')
121
+
122
+ colspan = cell.get_attribute('colspan').to_i
123
+
124
+ next unless colspan > 1
125
+
126
+ (1..colspan - 1).each do |j|
127
+ new_row.insert(i + j, cell)
128
+ end
129
+ end
130
+ new_row
131
+ end
132
+
133
+ ##
134
+ # Validate the list that represents the table header.
135
+ #
136
+ # @param header [Array<Array<Hatemile::Util::Html::HTMLDOMElement>>] The
137
+ # list that represents the table header.
138
+ # @return [Boolean] True if the table header is valid or false if the
139
+ # table header is not valid.
140
+ def valid_header?(header)
141
+ return false if header.empty?
142
+ length = -1
143
+ header.each do |row|
144
+ return false if row.empty?
145
+ length = row.size if length == -1
146
+ return false if row.size != length
147
+ end
148
+ true
149
+ end
150
+
151
+ ##
152
+ # Returns a list with ids of rows of same column.
153
+ #
154
+ # @param header [Array<Array<Hatemile::Util::Html::HTMLDOMElement>>] The
155
+ # list that represents the table header.
156
+ # @param index [Integer] The index of columns.
157
+ # @return [Array<String>] The list with ids of rows of same column.
158
+ def get_cells_headers_ids(header, index)
159
+ ids = []
160
+ header.each do |row|
161
+ if row[index].has_attribute?('scope') &&
162
+ row[index].get_attribute('scope') == 'col'
163
+ ids.push(row[index].get_attribute('id'))
164
+ end
165
+ end
166
+ ids
167
+ end
168
+
169
+ ##
170
+ # Associate the data cell with header cell of row.
171
+ #
172
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The table body or
173
+ # table footer.
174
+ # @return [void]
175
+ def associate_data_cells_with_header_cells_of_row(element)
176
+ table = get_model_table(element)
177
+ headers_ids = []
178
+ table.each do |row|
179
+ headers_ids.clear
180
+ row.each do |cell|
181
+ next unless cell.get_tag_name == 'TH' &&
182
+ Hatemile::Util::CommonFunctions.is_valid_element?(cell)
183
+
184
+ @id_generator.generate_id(cell)
185
+ headers_ids.push(cell.get_attribute('id'))
186
+ cell.set_attribute('scope', 'row')
187
+ end
188
+
189
+ next if headers_ids.empty?
190
+
191
+ row.each do |cell|
192
+ next unless cell.get_tag_name == 'TD' &&
193
+ Hatemile::Util::CommonFunctions.is_valid_element?(cell)
194
+
195
+ headers = cell.get_attribute('headers')
196
+ headers_ids.each do |header_id|
197
+ headers = Hatemile::Util::CommonFunctions.increase_in_list(
198
+ headers,
199
+ header_id
200
+ )
201
+ end
202
+ cell.set_attribute('headers', headers)
203
+ end
204
+ end
205
+ end
206
+
207
+ ##
208
+ # Set the scope of header cells of table header.
209
+ #
210
+ # @param table_header [Hatemile::Util::Html::HTMLDOMElement] The table
211
+ # header.
212
+ # @return [void]
213
+ def prepare_header_cells(table_header)
214
+ cells = @parser.find(table_header).find_children('tr').find_children(
215
+ 'th'
216
+ ).list_results
217
+ cells.each do |cell|
218
+ next unless Hatemile::Util::CommonFunctions.is_valid_element?(cell)
219
+
220
+ @id_generator.generate_id(cell)
221
+ cell.set_attribute('scope', 'col')
222
+ end
223
+ end
224
+
225
+ public
226
+
227
+ ##
228
+ # Initializes a new object that improve the accessibility of associations
229
+ # of parser.
230
+ #
231
+ # @param parser [Hatemile::Util::Html::HTMLDOMParser] The HTML parser.
232
+ def initialize(parser)
233
+ Hatemile::Helper.require_not_nil(parser)
234
+ Hatemile::Helper.require_valid_type(
235
+ parser,
236
+ Hatemile::Util::Html::HTMLDOMParser
237
+ )
238
+
239
+ @parser = parser
240
+ @id_generator = Hatemile::Util::IDGenerator.new('association')
241
+ end
242
+
243
+ ##
244
+ # @see Hatemile::AccessibleAssociation#associate_data_cells_with_header_cells
245
+ def associate_data_cells_with_header_cells(table)
246
+ header = @parser.find(table).find_children('thead').first_result
247
+ body = @parser.find(table).find_children('tbody').first_result
248
+ footer = @parser.find(table).find_children('tfoot').first_result
249
+ unless header.nil?
250
+ prepare_header_cells(header)
251
+
252
+ header_rows = get_model_table(header)
253
+ if !body.nil? && valid_header?(header_rows)
254
+ length_header = header_rows.first.size
255
+ fake_table = get_model_table(body)
256
+ unless footer.nil?
257
+ fake_table = fake_table.concat(get_model_table(footer))
258
+ end
259
+ fake_table.each do |row|
260
+ next unless row.size == length_header
261
+
262
+ row.each_with_index do |cell, index|
263
+ unless Hatemile::Util::CommonFunctions.is_valid_element?(cell)
264
+ next
265
+ end
266
+
267
+ headers_ids = get_cells_headers_ids(header_rows, index)
268
+ headers = cell.get_attribute('headers')
269
+ headers_ids.each do |headers_id|
270
+ headers = Hatemile::Util::CommonFunctions.increase_in_list(
271
+ headers,
272
+ headers_id
273
+ )
274
+ end
275
+ cell.set_attribute('headers', headers)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ associate_data_cells_with_header_cells_of_row(body) unless body.nil?
281
+ associate_data_cells_with_header_cells_of_row(footer) unless footer.nil?
282
+ end
283
+
284
+ ##
285
+ # @see Hatemile::AccessibleAssociation#associate_all_data_cells_with_header_cells
286
+ def associate_all_data_cells_with_header_cells
287
+ tables = @parser.find('table').list_results
288
+ tables.each do |table|
289
+ if Hatemile::Util::CommonFunctions.is_valid_element?(table)
290
+ associate_data_cells_with_header_cells(table)
291
+ end
292
+ end
293
+ end
294
+
295
+ ##
296
+ # @see Hatemile::AccessibleAssociation#associate_label_with_field
297
+ def associate_label_with_field(label)
298
+ return unless label.get_tag_name == 'LABEL'
299
+
300
+ if label.has_attribute?('for')
301
+ field = @parser.find("##{label.get_attribute('for')}").first_result
302
+ else
303
+ field = @parser.find(label).find_descendants(
304
+ 'input,select,textarea'
305
+ ).first_result
306
+
307
+ unless field.nil? ||
308
+ !Hatemile::Util::CommonFunctions.is_valid_element?(field)
309
+ @id_generator.generate_id(field)
310
+ label.set_attribute('for', field.get_attribute('id'))
311
+ end
312
+ end
313
+
314
+ return if field.nil?
315
+ return unless Hatemile::Util::CommonFunctions.is_valid_element?(field)
316
+
317
+ unless field.has_attribute?('aria-label')
318
+ field.set_attribute(
319
+ 'aria-label',
320
+ label.get_text_content.gsub(/[ \n\r\t]+/, ' ').strip
321
+ )
322
+ end
323
+
324
+ @id_generator.generate_id(label)
325
+ field.set_attribute(
326
+ 'aria-labelledby',
327
+ Hatemile::Util::CommonFunctions.increase_in_list(
328
+ field.get_attribute('aria-labelledby'),
329
+ label.get_attribute('id')
330
+ )
331
+ )
332
+ end
333
+
334
+ ##
335
+ # @see Hatemile::AccessibleAssociation#associate_all_labels_with_fields
336
+ def associate_all_labels_with_fields
337
+ labels = @parser.find('label').list_results
338
+ labels.each do |label|
339
+ if Hatemile::Util::CommonFunctions.is_valid_element?(label)
340
+ associate_label_with_field(label)
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,772 @@
1
+ # Licensed under the Apache License, Version 2.0 (the "License");
2
+ # you may not use this file except in compliance with the License.
3
+ # You may obtain a copy of the License at
4
+ #
5
+ # http://www.apache.org/licenses/LICENSE-2.0
6
+ #
7
+ # Unless required by applicable law or agreed to in writing, software
8
+ # distributed under the License is distributed on an "AS IS" BASIS,
9
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ # See the License for the specific language governing permissions and
11
+ # limitations under the License.
12
+
13
+ require File.join(File.dirname(File.dirname(__FILE__)), 'accessible_css')
14
+ require File.join(File.dirname(File.dirname(__FILE__)), 'helper')
15
+ require File.join(
16
+ File.dirname(File.dirname(__FILE__)),
17
+ 'util',
18
+ 'common_functions'
19
+ )
20
+ require File.join(File.dirname(File.dirname(__FILE__)), 'util', 'configure')
21
+ require File.join(File.dirname(File.dirname(__FILE__)), 'util', 'id_generator')
22
+ require File.join(
23
+ File.dirname(File.dirname(__FILE__)),
24
+ 'util',
25
+ 'css',
26
+ 'style_sheet_parser'
27
+ )
28
+ require File.join(
29
+ File.dirname(File.dirname(__FILE__)),
30
+ 'util',
31
+ 'html',
32
+ 'html_dom_parser'
33
+ )
34
+ require File.join(
35
+ File.dirname(File.dirname(__FILE__)),
36
+ 'util',
37
+ 'html',
38
+ 'html_dom_text_node'
39
+ )
40
+
41
+ ##
42
+ # The Hatemile module contains the interfaces with the acessibility solutions.
43
+ module Hatemile
44
+ ##
45
+ # The Hatemile::Implementation module contains the official implementation of
46
+ # interfaces solutions.
47
+ module Implementation
48
+ ##
49
+ # The AccessibleCSSImplementation class is official implementation of
50
+ # AccessibleCSS.
51
+ class AccessibleCSSImplementation < AccessibleCSS
52
+ public_class_method :new
53
+
54
+ ##
55
+ # The name of attribute for identify isolator elements.
56
+ DATA_ISOLATOR_ELEMENT = 'data-auxiliarspan'.freeze
57
+
58
+ ##
59
+ # The name of attribute for identify the element created or modified to
60
+ # support speak property.
61
+ DATA_SPEAK = 'data-cssspeak'.freeze
62
+
63
+ ##
64
+ # The name of attribute for identify the element created or modified to
65
+ # support speak-as property.
66
+ DATA_SPEAK_AS = 'data-cssspeakas'.freeze
67
+
68
+ ##
69
+ # The valid element tags for inherit the speak and speak-as properties.
70
+ VALID_INHERIT_TAGS = %w[
71
+ SPAN A RT DFN ABBR Q CITE EM TIME VAR SAMP I B SUB SUP SMALL STRONG
72
+ MARK RUBY INS DEL KBD BDO CODE P FIGCAPTION FIGURE PRE DIV OL UL LI
73
+ BLOCKQUOTE DL DT DD FIELDSET LEGEND LABEL FORM BODY ASIDE ADDRESS H1 H2
74
+ H3 H4 H5 H6 SECTION HEADER NAV ARTICLE FOOTER HGROUP CAPTION SUMMARY
75
+ DETAILS TABLE TR TD TH TBODY THEAD TFOOT
76
+ ].freeze
77
+
78
+ ##
79
+ # The valid element tags for speak and speak-as properties.
80
+ VALID_TAGS = %w[
81
+ SPAN A RT DFN ABBR Q CITE EM TIME VAR SAMP I B SUB SUP SMALL STRONG MARK
82
+ RUBY INS DEL KBD BDO CODE P FIGCAPTION FIGURE PRE DIV LI BLOCKQUOTE DT
83
+ DD FIELDSET LEGEND LABEL FORM BODY ASIDE ADDRESS H1 H2 H3 H4 H5 H6
84
+ SECTION HEADER NAV ARTICLE FOOTER CAPTION SUMMARY DETAILS TD TH
85
+ ].freeze
86
+
87
+ protected
88
+
89
+ ##
90
+ # The operation method of _speak_as method for spell-out.
91
+ #
92
+ # @param content [String] The text content of element.
93
+ # @param index [Integer] The index of pattern in text content of element.
94
+ # @param children [Array<Hatemile::Util::Html::HTMLDOMElement>] The
95
+ # children of element.
96
+ # @return [Array<Hatemile::Util::Html::HTMLDOMElement>] The new children
97
+ # of element.
98
+ def operation_speak_as_spell_out(content, index, children)
99
+ children.push(
100
+ create_content_element(content[0..index], 'spell-out')
101
+ )
102
+
103
+ children.push(create_aural_content_element(' ', 'spell-out'))
104
+
105
+ children
106
+ end
107
+
108
+ ##
109
+ # The operation method of _speak_as method for literal-punctuation.
110
+ #
111
+ # @param content [String] The text content of element.
112
+ # @param index [Integer] The index of pattern in text content of element.
113
+ # @param children [Array<Hatemile::Util::Html::HTMLDOMElement>] The
114
+ # children of element.
115
+ # @return [Array<Hatemile::Util::Html::HTMLDOMElement>] The new children
116
+ # of element.
117
+ def operation_speak_as_literal_punctuation(content, index, children)
118
+ data_property_value = 'literal-punctuation'
119
+ unless index.zero?
120
+ children.push(
121
+ create_content_element(content[0..(index - 1)], data_property_value)
122
+ )
123
+ end
124
+ children.push(
125
+ create_aural_content_element(
126
+ " #{get_description_of_symbol(content[index..index])} ",
127
+ data_property_value
128
+ )
129
+ )
130
+
131
+ children.push(
132
+ create_visual_content_element(
133
+ content[index..index],
134
+ data_property_value
135
+ )
136
+ )
137
+
138
+ children
139
+ end
140
+
141
+ ##
142
+ # The operation method of _speak_as method for no-punctuation.
143
+ #
144
+ # @param content [String] The text content of element.
145
+ # @param index [Integer] The index of pattern in text content of element.
146
+ # @param children [Array<Hatemile::Util::Html::HTMLDOMElement>] The
147
+ # children of element.
148
+ # @return [Array<Hatemile::Util::Html::HTMLDOMElement>] The new children
149
+ # of element.
150
+ def operation_speak_as_no_punctuation(content, index, children)
151
+ unless index.zero?
152
+ children.push(
153
+ create_content_element(content[0..(index - 1)], 'no-punctuation')
154
+ )
155
+ end
156
+ children.push(
157
+ create_visual_content_element(
158
+ content[index..index],
159
+ 'no-punctuation'
160
+ )
161
+ )
162
+
163
+ children
164
+ end
165
+
166
+ ##
167
+ # The operation method of _speak_as method for digits.
168
+ #
169
+ # @param content [String] The text content of element.
170
+ # @param index [Integer] The index of pattern in text content of element.
171
+ # @param children [Array<Hatemile::Util::Html::HTMLDOMElement>] The
172
+ # children of element.
173
+ # @return [Array<Hatemile::Util::Html::HTMLDOMElement>] The new children
174
+ # of element.
175
+ def operation_speak_as_digits(content, index, children)
176
+ data_property_value = 'digits'
177
+ unless index.zero?
178
+ children.push(
179
+ create_content_element(content[0..(index - 1)], data_property_value)
180
+ )
181
+ end
182
+ children.push(create_aural_content_element(' ', data_property_value))
183
+
184
+ children.push(
185
+ create_content_element(
186
+ content[index..index],
187
+ data_property_value
188
+ )
189
+ )
190
+
191
+ children
192
+ end
193
+
194
+ ##
195
+ # Load the symbols with configuration.
196
+ #
197
+ # @param file_name [String] The file path of symbol configuration.
198
+ def set_symbols(file_name)
199
+ @symbols = []
200
+ if file_name.nil?
201
+ file_name = File.join(
202
+ File.dirname(File.dirname(File.dirname(__FILE__))),
203
+ 'symbols.xml'
204
+ )
205
+ end
206
+ document = REXML::Document.new(File.read(file_name))
207
+ document.elements.each('symbols/symbol') do |symbol_xml|
208
+ symbol = {}
209
+ symbol[:symbol] = symbol_xml.attribute('symbol').value
210
+ symbol[:description] = @configure.get_parameter(
211
+ symbol_xml.attribute('description').value
212
+ )
213
+ @symbols.push(symbol)
214
+ end
215
+ end
216
+
217
+ ##
218
+ # Returns the description of symbol.
219
+ #
220
+ # @param symbol [String] The symbol.
221
+ # @return [String] The description of symbol.
222
+ def get_description_of_symbol(symbol)
223
+ @symbols.each do |dict_symbol|
224
+ return dict_symbol[:description] if dict_symbol[:symbol] == symbol
225
+ end
226
+ nil
227
+ end
228
+
229
+ ##
230
+ # Returns the regular expression to search all symbols.
231
+ #
232
+ # @return [String] The regular expression to search all symbols.
233
+ def get_regular_expression_of_symbols
234
+ regular_expression = nil
235
+ @symbols.each do |symbol|
236
+ formated_symbol = Regexp.escape(symbol[:symbol])
237
+ regular_expression = if regular_expression.nil?
238
+ "(#{formated_symbol})"
239
+ else
240
+ "#{regular_expression}|(#{formated_symbol})"
241
+ end
242
+ end
243
+ Regexp.new(regular_expression)
244
+ end
245
+
246
+ ##
247
+ # Check that the children of element can be manipulated to apply the CSS
248
+ # properties.
249
+ #
250
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
251
+ # @return [Boolean] True if the children of element can be manipulated to
252
+ # apply the CSS properties or false if the children of element cannot be
253
+ # manipulated to apply the CSS properties.
254
+ def is_valid_inherit_element?(element)
255
+ VALID_INHERIT_TAGS.include?(element.get_tag_name) &&
256
+ Hatemile::Util::CommonFunctions.is_valid_element?(element)
257
+ end
258
+
259
+ ##
260
+ # Check that the element can be manipulated to apply the CSS properties.
261
+ #
262
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
263
+ # @return [Boolean] True if the element can be manipulated to apply the
264
+ # CSS properties or False if the element cannot be manipulated to apply
265
+ # the CSS properties.
266
+ def is_valid_element?(element)
267
+ VALID_TAGS.include?(element.get_tag_name)
268
+ end
269
+
270
+ ##
271
+ # Isolate text nodes of element nodes.
272
+ #
273
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
274
+ def isolate_text_node(element)
275
+ unless element.has_children_elements? && is_valid_element?(element)
276
+ return
277
+ end
278
+
279
+ element.get_children.each do |child_node|
280
+ next unless child_node.is_a?(Hatemile::Util::Html::HTMLDOMTextNode)
281
+
282
+ span = @html_parser.create_element('span')
283
+ span.set_attribute(DATA_ISOLATOR_ELEMENT, 'true')
284
+ span.append_text(child_node.get_text_content)
285
+
286
+ child_node.replace_node(span)
287
+ end
288
+ element.get_children_elements.each do |child|
289
+ isolate_text_node(child)
290
+ end
291
+ end
292
+
293
+ ##
294
+ # Replace the element by own text content.
295
+ #
296
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
297
+ def replace_element_by_own_content(element)
298
+ if element.has_children_elements?
299
+ element.get_children_elements.each do |child|
300
+ element.insert_before(child)
301
+ end
302
+ element.remove_node
303
+ elsif element.has_children?
304
+ element.replace_node(element.get_first_node_child)
305
+ end
306
+ end
307
+
308
+ ##
309
+ # Visit and execute a operation in element and descendants.
310
+ #
311
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
312
+ # @param operation [Method] The operation to be executed.
313
+ def visit(element, operation)
314
+ return unless is_valid_inherit_element?(element)
315
+
316
+ if element.has_children_elements?
317
+ element.get_children_elements.each do |child|
318
+ visit(child, operation)
319
+ end
320
+ elsif is_valid_element?(element)
321
+ operation.call(element)
322
+ end
323
+ end
324
+
325
+ ##
326
+ # Create a element to show the content.
327
+ #
328
+ # @param content [String] The text content of element.
329
+ # @param data_property_value [String] The value of custom attribute used
330
+ # to identify the fix.
331
+ # @return [Hatemile::Util::Html::HTMLDOMElement] The element to show the
332
+ # content.
333
+ def create_content_element(content, data_property_value)
334
+ content_element = @html_parser.create_element('span')
335
+ content_element.set_attribute(DATA_ISOLATOR_ELEMENT, 'true')
336
+ content_element.set_attribute(DATA_SPEAK_AS, data_property_value)
337
+ content_element.append_text(content)
338
+ content_element
339
+ end
340
+
341
+ ##
342
+ # Create a element to show the content, only to aural displays.
343
+ #
344
+ # @param content [String] The text content of element.
345
+ # @param data_property_value [String] The value of custom attribute used
346
+ # to identify the fix.
347
+ # @return [Hatemile::Util::Html::HTMLDOMElement] The element to show the
348
+ # content.
349
+ def create_aural_content_element(content, data_property_value)
350
+ content_element = create_content_element(content, data_property_value)
351
+ content_element.set_attribute('unselectable', 'on')
352
+ content_element.set_attribute('class', 'screen-reader-only')
353
+ content_element
354
+ end
355
+
356
+ ##
357
+ # Create a element to show the content, only to visual displays.
358
+ #
359
+ # @param content [String] The text content of element.
360
+ # @param data_property_value [String] The value of custom attribute used
361
+ # to identify the fix.
362
+ # @return [Hatemile::Util::Html::HTMLDOMElement] The element to show the
363
+ # content.
364
+ def create_visual_content_element(content, data_property_value)
365
+ content_element = create_content_element(content, data_property_value)
366
+ content_element.set_attribute('aria-hidden', 'true')
367
+ content_element.set_attribute('role', 'presentation')
368
+ content_element
369
+ end
370
+
371
+ ##
372
+ # Speak the content of element only.
373
+ #
374
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
375
+ def speak_normal(element)
376
+ return unless element.has_attribute?(DATA_SPEAK)
377
+
378
+ if element.get_attribute(DATA_SPEAK) == 'none' &&
379
+ !element.has_attribute?(DATA_ISOLATOR_ELEMENT)
380
+ element.remove_attribute('role')
381
+ element.remove_attribute('aria-hidden')
382
+ element.remove_attribute(DATA_SPEAK)
383
+ else
384
+ replace_element_by_own_content(element)
385
+ end
386
+ end
387
+
388
+ ##
389
+ # Speak the content of element and descendants.
390
+ #
391
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
392
+ def speak_normal_inherit(element)
393
+ visit(element, method(:speak_normal))
394
+
395
+ element.normalize
396
+ end
397
+
398
+ ##
399
+ # No speak any content of element only.
400
+ #
401
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
402
+ def speak_none(element)
403
+ element.set_attribute('role', 'presentation')
404
+ element.set_attribute('aria-hidden', 'true')
405
+ element.set_attribute(DATA_SPEAK, 'none')
406
+ end
407
+
408
+ ##
409
+ # No speak any content of element and descendants.
410
+ #
411
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
412
+ def speak_none_inherit(element)
413
+ isolate_text_node(element)
414
+
415
+ visit(element, method(:speak_none))
416
+ end
417
+
418
+ ##
419
+ # Execute a operation by regular expression for element only.
420
+ #
421
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
422
+ # @param regular_expression [Regexp] The regular expression.
423
+ # @param data_property_value [String] The value of custom attribute used
424
+ # to identify the fix.
425
+ # @param operation [Method] The operation to be executed.
426
+ def speak_as(element, regular_expression, data_property_value, operation)
427
+ children = []
428
+ content = element.get_text_content
429
+ until content.empty?
430
+ index = content.index(regular_expression)
431
+
432
+ break if index.nil?
433
+
434
+ children = operation.call(content, index, children)
435
+
436
+ new_index = index + 1
437
+ content = content[new_index..-1]
438
+ end
439
+
440
+ return if children.empty?
441
+
442
+ unless content.empty?
443
+ children.push(create_content_element(content, data_property_value))
444
+ end
445
+ element.get_first_node_child.remove_node while element.has_children?
446
+ children.each do |child|
447
+ element.append_element(child)
448
+ end
449
+ end
450
+
451
+ ##
452
+ # Revert changes of a speak_as method for element and descendants.
453
+ #
454
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
455
+ # @param data_property_value [String] The value of custom attribute used
456
+ # to identify the fix.
457
+ def reverse_speak_as(element, data_property_value)
458
+ data_property = "[#{DATA_SPEAK_AS}=\"#{data_property_value}\"]"
459
+
460
+ auxiliar_elements = @html_parser.find(element).find_descendants(
461
+ "#{data_property}[unselectable=\"on\"]"
462
+ ).list_results
463
+ auxiliar_elements.each(&:remove_node)
464
+
465
+ content_elements = @html_parser.find(element).find_descendants(
466
+ "#{data_property}[#{DATA_ISOLATOR_ELEMENT}=\"true\"],
467
+ #{data_property}[aria-hidden]"
468
+ ).list_results
469
+ content_elements.each do |content_element|
470
+ replace_element_by_own_content(content_element)
471
+ end
472
+ element.normalize
473
+ end
474
+
475
+ ##
476
+ # Use the default speak configuration of user agent for element and
477
+ # descendants.
478
+ #
479
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
480
+ def speak_as_normal(element)
481
+ reverse_speak_as(element, 'spell-out')
482
+ reverse_speak_as(element, 'literal-punctuation')
483
+ reverse_speak_as(element, 'no-punctuation')
484
+ reverse_speak_as(element, 'digits')
485
+ end
486
+
487
+ ##
488
+ # Speak one letter at a time for each word for element only.
489
+ #
490
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
491
+ def speak_as_spell_out(element)
492
+ speak_as(
493
+ element,
494
+ /[a-zA-Z]/,
495
+ 'spell-out',
496
+ method(:operation_speak_as_spell_out)
497
+ )
498
+ end
499
+
500
+ ##
501
+ # Speak one letter at a time for each word for elements and descendants.
502
+ #
503
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
504
+ def speak_as_spell_out_inherit(element)
505
+ reverse_speak_as(element, 'spell-out')
506
+
507
+ isolate_text_node(element)
508
+
509
+ visit(element, method(:speak_as_spell_out))
510
+ end
511
+
512
+ ##
513
+ # Speak the punctuation for elements only.
514
+ #
515
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
516
+ def speak_as_literal_punctuation(element)
517
+ speak_as(
518
+ element,
519
+ get_regular_expression_of_symbols,
520
+ 'literal-punctuation',
521
+ method(:operation_speak_as_literal_punctuation)
522
+ )
523
+ end
524
+
525
+ ##
526
+ # Speak the punctuation for elements and descendants.
527
+ #
528
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
529
+ def speak_as_literal_punctuation_inherit(element)
530
+ reverse_speak_as(element, 'literal-punctuation')
531
+ reverse_speak_as(element, 'no-punctuation')
532
+
533
+ isolate_text_node(element)
534
+
535
+ visit(element, method(:speak_as_literal_punctuation))
536
+ end
537
+
538
+ ##
539
+ # No speak the punctuation for element only.
540
+ #
541
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
542
+ def speak_as_no_punctuation(element)
543
+ escape_punctuation = Regexp.escape('!"#$%&\'()*+,-./:;<=>?@[]^_`{|}~\\')
544
+ speak_as(
545
+ element,
546
+ Regexp.new("[#{escape_punctuation}]"),
547
+ 'no-punctuation',
548
+ method(:operation_speak_as_no_punctuation)
549
+ )
550
+ end
551
+
552
+ ##
553
+ # No speak the punctuation for element and descendants.
554
+ #
555
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
556
+ def speak_as_no_punctuation_inherit(element)
557
+ reverse_speak_as(element, 'literal-punctuation')
558
+ reverse_speak_as(element, 'no-punctuation')
559
+
560
+ isolate_text_node(element)
561
+
562
+ visit(element, method(:speak_as_no_punctuation))
563
+ end
564
+
565
+ ##
566
+ # Speak the digit at a time for each number for element only.
567
+ #
568
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
569
+ def speak_as_digits(element)
570
+ speak_as(element, /[0-9]/, 'digits', method(:operation_speak_as_digits))
571
+ end
572
+
573
+ ##
574
+ # Speak the digit at a time for each number for element and descendants.
575
+ #
576
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
577
+ def speak_as_digits_inherit(element)
578
+ reverse_speak_as(element, 'digits')
579
+
580
+ isolate_text_node(element)
581
+
582
+ visit(element, method(:speak_as_digits))
583
+ end
584
+
585
+ ##
586
+ # Speaks the numbers for element and descendants as a word number.
587
+ #
588
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
589
+ def speak_as_continuous_inherit(element)
590
+ reverse_speak_as(element, 'digits')
591
+ end
592
+
593
+ ##
594
+ # The cells headers will be spoken for every data cell for element and
595
+ # descendants.
596
+ #
597
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
598
+ def speak_header_always_inherit(element)
599
+ speak_header_once_inherit(element)
600
+
601
+ cell_elements = @html_parser.find(element).find_descendants(
602
+ 'td[headers],th[headers]'
603
+ ).list_results
604
+ accessible_display = AccessibleDisplayImplementation(
605
+ @html_parser,
606
+ @configure
607
+ )
608
+ cell_elements.each do |cell_element|
609
+ accessible_display.display_cell_header(cell_element)
610
+ end
611
+ end
612
+
613
+ ##
614
+ # The cells headers will be spoken one time for element and descendants.
615
+ #
616
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
617
+ def speak_header_once_inherit(element)
618
+ header_elements = @html_parser.find(element).find_descendants(
619
+ "[#{DATA_ATTRIBUTE_HEADERS_OF}]"
620
+ ).list_results
621
+ header_elements.each(&:remove_node)
622
+ end
623
+
624
+ ##
625
+ # Provide the CSS features of speaking and speech properties in element.
626
+ #
627
+ # @param element [Hatemile::Util::Html::HTMLDOMElement] The element.
628
+ # @param rule [Hatemile::Util::Css::StyleSheetRule] The stylesheet rule.
629
+ def provide_speak_properties_with_rule(element, rule)
630
+ if rule.has_property?('speak')
631
+ declarations = rule.get_declarations('speak')
632
+ declarations.each do |declaration|
633
+ property_value = declaration.get_value
634
+ if property_value == 'none'
635
+ speak_none_inherit(element)
636
+ elsif property_value == 'normal'
637
+ speak_normal_inherit(element)
638
+ elsif property_value == 'spell-out'
639
+ speak_as_spell_out_inherit(element)
640
+ end
641
+ end
642
+ end
643
+ if rule.has_property?('speak-as')
644
+ declarations = rule.get_declarations('speak-as')
645
+ declarations.each do |declaration|
646
+ speak_as_values = declaration.get_values
647
+ speak_as_normal(element)
648
+ speak_as_values.each do |speak_as_value|
649
+ if speak_as_value == 'spell-out'
650
+ speak_as_spell_out_inherit(element)
651
+ elsif speak_as_value == 'literal-punctuation'
652
+ speak_as_literal_punctuation_inherit(element)
653
+ elsif speak_as_value == 'no-punctuation'
654
+ speak_as_no_punctuation_inherit(element)
655
+ elsif speak_as_value == 'digits'
656
+ speak_as_digits_inherit(element)
657
+ end
658
+ end
659
+ end
660
+ end
661
+ if rule.has_property?('speak-punctuation')
662
+ declarations = rule.get_declarations('speak-punctuation')
663
+ declarations.each do |declaration|
664
+ property_value = declaration.get_value
665
+ if property_value == 'code'
666
+ speak_as_literal_punctuation_inherit(element)
667
+ elsif property_value == 'none'
668
+ speak_as_no_punctuation_inherit(element)
669
+ end
670
+ end
671
+ end
672
+ if rule.has_property?('speak-numeral')
673
+ declarations = rule.get_declarations('speak-numeral')
674
+ declarations.each do |declaration|
675
+ property_value = declaration.get_value
676
+ if property_value == 'digits'
677
+ speak_as_digits_inherit(element)
678
+ elsif property_value == 'continuous'
679
+ speak_as_continuous_inherit(element)
680
+ end
681
+ end
682
+ end
683
+
684
+ return unless rule.has_property?('speak-header')
685
+
686
+ declarations = rule.get_declarations('speak-header')
687
+ declarations.each do |declaration|
688
+ property_value = declaration.get_value
689
+ if property_value == 'always'
690
+ speak_header_always_inherit(element)
691
+ elsif property_value == 'once'
692
+ speak_header_once_inherit(element)
693
+ end
694
+ end
695
+ end
696
+
697
+ public
698
+
699
+ ##
700
+ # Initializes a new object that improve the accessibility of associations
701
+ # of parser.
702
+ #
703
+ # @param html_parser [Hatemile::Util::Html::HTMLDOMParser] The HTML
704
+ # parser.
705
+ # @param css_parser [Hatemile::Util::Css::StyleSheetParser] The CSS
706
+ # parser.
707
+ # @param configure [Hatemile::Util::Configure] The configuration of
708
+ # HaTeMiLe.
709
+ # @param symbol_file_name [String] The file path of symbol configuration.
710
+ def initialize(html_parser, css_parser, configure, symbol_file_name = nil)
711
+ Hatemile::Helper.require_not_nil(html_parser, css_parser, configure)
712
+ Hatemile::Helper.require_valid_type(
713
+ html_parser,
714
+ Hatemile::Util::Html::HTMLDOMParser
715
+ )
716
+ Hatemile::Helper.require_valid_type(
717
+ css_parser,
718
+ Hatemile::Util::Css::StyleSheetParser
719
+ )
720
+ Hatemile::Helper.require_valid_type(
721
+ configure,
722
+ Hatemile::Util::Configure
723
+ )
724
+ Hatemile::Helper.require_valid_type(symbol_file_name, String)
725
+
726
+ @html_parser = html_parser
727
+ @css_parser = css_parser
728
+ @configure = configure
729
+ @id_generator = Hatemile::Util::IDGenerator.new('css')
730
+ set_symbols(symbol_file_name)
731
+ end
732
+
733
+ ##
734
+ # @see Hatemile::AccessibleCSS#provide_speak_properties
735
+ def provide_speak_properties(element)
736
+ rules = @css_parser.get_rules(
737
+ %w[speak speak-punctuation speak-numeral speak-header speak-as]
738
+ )
739
+ rules.each do |rule|
740
+ speak_elements = @html_parser.find(rule.get_selector).list_results
741
+ if speak_elements.include?(element)
742
+ provide_speak_properties_with_rule(element, rule)
743
+ end
744
+ end
745
+ end
746
+
747
+ ##
748
+ # @see Hatemile::AccessibleCSS#provide_all_speak_properties
749
+ def provide_all_speak_properties
750
+ selector = nil
751
+ rules = @css_parser.get_rules(
752
+ %w[speak speak-punctuation speak-numeral speak-header speak-as]
753
+ )
754
+ rules.each do |rule|
755
+ selector = if selector.nil?
756
+ rule.get_selector
757
+ else
758
+ "#{selector},#{rule.get_selector}"
759
+ end
760
+ end
761
+
762
+ return if selector.nil?
763
+
764
+ @html_parser.find(selector).list_results.each do |element|
765
+ if Hatemile::Util::CommonFunctions.is_valid_element?(element)
766
+ provide_speak_properties(element)
767
+ end
768
+ end
769
+ end
770
+ end
771
+ end
772
+ end