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