1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/accessibility/ax_text_utils.h"
6 
7 #include <algorithm>
8 
9 #include "base/i18n/break_iterator.h"
10 #include "base/logging.h"
11 #include "base/numerics/safe_conversions.h"
12 #include "base/optional.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "ui/accessibility/ax_enums.mojom.h"
16 #include "ui/base/l10n/l10n_util.h"
17 #include "ui/strings/grit/ui_strings.h"
18 
19 namespace ui {
20 
21 namespace {
22 
ICUBreakTypeForBoundaryType(ax::mojom::TextBoundary boundary)23 base::i18n::BreakIterator::BreakType ICUBreakTypeForBoundaryType(
24     ax::mojom::TextBoundary boundary) {
25   switch (boundary) {
26     case ax::mojom::TextBoundary::kCharacter:
27       return base::i18n::BreakIterator::BREAK_CHARACTER;
28     case ax::mojom::TextBoundary::kSentenceStart:
29       return base::i18n::BreakIterator::BREAK_SENTENCE;
30     case ax::mojom::TextBoundary::kWordStart:
31     case ax::mojom::TextBoundary::kWordStartOrEnd:
32       return base::i18n::BreakIterator::BREAK_WORD;
33     // These are currently unused since line breaking is done via an array of
34     // line break offsets, and object boundary by finding no boundary within the
35     // current node.
36     case ax::mojom::TextBoundary::kObject:
37     case ax::mojom::TextBoundary::kLineStart:
38     case ax::mojom::TextBoundary::kParagraphStart:
39       return base::i18n::BreakIterator::BREAK_NEWLINE;
40     default:
41       NOTREACHED() << boundary;
42       return base::i18n::BreakIterator::BREAK_NEWLINE;
43   }
44 }
45 
46 }  // namespace
47 
48 // line_breaks is a Misnomer. Blink provides the start offsets of each line
49 // not the line breaks.
50 // TODO(nektar): Rename line_breaks a11y attribute and variable references.
FindAccessibleTextBoundary(const base::string16 & text,const std::vector<int> & line_breaks,ax::mojom::TextBoundary boundary,size_t start_offset,ax::mojom::MoveDirection direction,ax::mojom::TextAffinity affinity)51 size_t FindAccessibleTextBoundary(const base::string16& text,
52                                   const std::vector<int>& line_breaks,
53                                   ax::mojom::TextBoundary boundary,
54                                   size_t start_offset,
55                                   ax::mojom::MoveDirection direction,
56                                   ax::mojom::TextAffinity affinity) {
57   size_t text_size = text.size();
58   DCHECK_LE(start_offset, text_size);
59 
60   base::i18n::BreakIterator::BreakType break_type =
61       ICUBreakTypeForBoundaryType(boundary);
62   base::i18n::BreakIterator break_iter(text, break_type);
63   if (boundary == ax::mojom::TextBoundary::kCharacter ||
64       boundary == ax::mojom::TextBoundary::kSentenceStart ||
65       boundary == ax::mojom::TextBoundary::kWordStart ||
66       boundary == ax::mojom::TextBoundary::kWordStartOrEnd) {
67     if (!break_iter.Init())
68       return start_offset;
69   }
70 
71   if (boundary == ax::mojom::TextBoundary::kLineStart) {
72     if (direction == ax::mojom::MoveDirection::kForward) {
73       for (int line_break : line_breaks) {
74         size_t clamped_line_break = std::max(0, line_break);
75         if ((affinity == ax::mojom::TextAffinity::kDownstream &&
76              clamped_line_break > start_offset) ||
77             (affinity == ax::mojom::TextAffinity::kUpstream &&
78              clamped_line_break >= start_offset)) {
79           return clamped_line_break;
80         }
81       }
82       return text_size;
83     } else {
84       for (size_t j = line_breaks.size(); j != 0; --j) {
85         size_t line_break = line_breaks[j - 1] >= 0 ? line_breaks[j - 1] : 0;
86         if ((affinity == ax::mojom::TextAffinity::kDownstream &&
87              line_break <= start_offset) ||
88             (affinity == ax::mojom::TextAffinity::kUpstream &&
89              line_break < start_offset)) {
90           return line_break;
91         }
92       }
93       return 0;
94     }
95   }
96 
97   size_t result = start_offset;
98   for (;;) {
99     size_t pos;
100     if (direction == ax::mojom::MoveDirection::kForward) {
101       if (result >= text_size)
102         return text_size;
103       pos = result;
104     } else {
105       if (result == 0)
106         return 0;
107       pos = result - 1;
108     }
109 
110     switch (boundary) {
111       case ax::mojom::TextBoundary::kLineStart:
112         NOTREACHED() << boundary;  // This is handled above.
113         return result;
114       case ax::mojom::TextBoundary::kCharacter:
115         if (break_iter.IsGraphemeBoundary(result)) {
116           // If we are searching forward and we are still at the start offset,
117           // we need to find the next character.
118           if (direction == ax::mojom::MoveDirection::kBackward ||
119               result != start_offset)
120             return result;
121         }
122         break;
123       case ax::mojom::TextBoundary::kWordStart:
124         if (break_iter.IsStartOfWord(result)) {
125           // If we are searching forward and we are still at the start offset,
126           // we need to find the next word.
127           if (direction == ax::mojom::MoveDirection::kBackward ||
128               result != start_offset)
129             return result;
130         }
131         break;
132       case ax::mojom::TextBoundary::kWordStartOrEnd:
133         if (break_iter.IsStartOfWord(result)) {
134           // If we are searching forward and we are still at the start offset,
135           // we need to find the next word.
136           if (direction == ax::mojom::MoveDirection::kBackward ||
137               result != start_offset)
138             return result;
139         } else if (break_iter.IsEndOfWord(result)) {
140           // If we are searching backward and we are still at the end offset, we
141           // need to find the previous word.
142           if (direction == ax::mojom::MoveDirection::kForward ||
143               result != start_offset)
144             return result;
145         }
146         break;
147       case ax::mojom::TextBoundary::kSentenceStart:
148         if (break_iter.IsSentenceBoundary(result)) {
149           // If we are searching forward and we are still at the start offset,
150           // we need to find the next sentence.
151           if (direction == ax::mojom::MoveDirection::kBackward ||
152               result != start_offset) {
153             // ICU sometimes returns sentence boundaries in the whitespace
154             // between sentences. For the purposes of accessibility, we want to
155             // include all whitespace at the end of a sentence. We move the
156             // boundary past the last whitespace offset. This works the same for
157             // backwards and forwards searches.
158             while (result < text_size &&
159                    base::IsUnicodeWhitespace(text[result]))
160               result++;
161             return result;
162           }
163         }
164         break;
165       case ax::mojom::TextBoundary::kParagraphStart:
166         if (text[pos] == '\n')
167           return result;
168         break;
169       default:
170         break;
171     }
172 
173     if (direction == ax::mojom::MoveDirection::kForward) {
174       result++;
175     } else {
176       result--;
177     }
178   }
179 }
180 
ActionVerbToLocalizedString(const ax::mojom::DefaultActionVerb action_verb)181 base::string16 ActionVerbToLocalizedString(
182     const ax::mojom::DefaultActionVerb action_verb) {
183   switch (action_verb) {
184     case ax::mojom::DefaultActionVerb::kNone:
185       return base::string16();
186     case ax::mojom::DefaultActionVerb::kActivate:
187       return l10n_util::GetStringUTF16(IDS_AX_ACTIVATE_ACTION_VERB);
188     case ax::mojom::DefaultActionVerb::kCheck:
189       return l10n_util::GetStringUTF16(IDS_AX_CHECK_ACTION_VERB);
190     case ax::mojom::DefaultActionVerb::kClick:
191       return l10n_util::GetStringUTF16(IDS_AX_CLICK_ACTION_VERB);
192     case ax::mojom::DefaultActionVerb::kClickAncestor:
193       return l10n_util::GetStringUTF16(IDS_AX_CLICK_ANCESTOR_ACTION_VERB);
194     case ax::mojom::DefaultActionVerb::kJump:
195       return l10n_util::GetStringUTF16(IDS_AX_JUMP_ACTION_VERB);
196     case ax::mojom::DefaultActionVerb::kOpen:
197       return l10n_util::GetStringUTF16(IDS_AX_OPEN_ACTION_VERB);
198     case ax::mojom::DefaultActionVerb::kPress:
199       return l10n_util::GetStringUTF16(IDS_AX_PRESS_ACTION_VERB);
200     case ax::mojom::DefaultActionVerb::kSelect:
201       return l10n_util::GetStringUTF16(IDS_AX_SELECT_ACTION_VERB);
202     case ax::mojom::DefaultActionVerb::kUncheck:
203       return l10n_util::GetStringUTF16(IDS_AX_UNCHECK_ACTION_VERB);
204   }
205   NOTREACHED();
206   return base::string16();
207 }
208 
209 // Some APIs on Linux and Windows need to return non-localized action names.
ActionVerbToUnlocalizedString(const ax::mojom::DefaultActionVerb action_verb)210 base::string16 ActionVerbToUnlocalizedString(
211     const ax::mojom::DefaultActionVerb action_verb) {
212   switch (action_verb) {
213     case ax::mojom::DefaultActionVerb::kNone:
214       return base::UTF8ToUTF16("none");
215     case ax::mojom::DefaultActionVerb::kActivate:
216       return base::UTF8ToUTF16("activate");
217     case ax::mojom::DefaultActionVerb::kCheck:
218       return base::UTF8ToUTF16("check");
219     case ax::mojom::DefaultActionVerb::kClick:
220       return base::UTF8ToUTF16("click");
221     case ax::mojom::DefaultActionVerb::kClickAncestor:
222       return base::UTF8ToUTF16("click-ancestor");
223     case ax::mojom::DefaultActionVerb::kJump:
224       return base::UTF8ToUTF16("jump");
225     case ax::mojom::DefaultActionVerb::kOpen:
226       return base::UTF8ToUTF16("open");
227     case ax::mojom::DefaultActionVerb::kPress:
228       return base::UTF8ToUTF16("press");
229     case ax::mojom::DefaultActionVerb::kSelect:
230       return base::UTF8ToUTF16("select");
231     case ax::mojom::DefaultActionVerb::kUncheck:
232       return base::UTF8ToUTF16("uncheck");
233   }
234   NOTREACHED();
235   return base::string16();
236 }
237 
GetWordStartOffsets(const base::string16 & text)238 std::vector<int> GetWordStartOffsets(const base::string16& text) {
239   std::vector<int> word_starts;
240   base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD);
241   if (!iter.Init())
242     return word_starts;
243   // iter.Advance() returns false if we've run past end of the text.
244   while (iter.Advance()) {
245     if (!iter.IsWord())
246       continue;
247     word_starts.push_back(
248         base::checked_cast<int>(iter.prev()) /* start index */);
249   }
250   return word_starts;
251 }
252 
GetWordEndOffsets(const base::string16 & text)253 std::vector<int> GetWordEndOffsets(const base::string16& text) {
254   std::vector<int> word_ends;
255   base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD);
256   if (!iter.Init())
257     return word_ends;
258   // iter.Advance() returns false if we've run past end of the text.
259   while (iter.Advance()) {
260     if (!iter.IsWord())
261       continue;
262     word_ends.push_back(base::checked_cast<int>(iter.pos()) /* end index */);
263   }
264   return word_ends;
265 }
266 
267 }  // namespace ui
268