| // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/ax_text_utils.h" |
| |
| #include "base/i18n/break_iterator.h" |
| #include "base/logging.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/optional.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/strings/grit/ui_strings.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| base::i18n::BreakIterator::BreakType ICUBreakTypeForBoundaryType( |
| AXTextBoundary boundary) { |
| switch (boundary) { |
| case AXTextBoundary::kCharacter: |
| return base::i18n::BreakIterator::BREAK_CHARACTER; |
| case AXTextBoundary::kSentenceStart: |
| return base::i18n::BreakIterator::BREAK_SENTENCE; |
| case AXTextBoundary::kWordStart: |
| case AXTextBoundary::kWordStartOrEnd: |
| return base::i18n::BreakIterator::BREAK_WORD; |
| // These are currently unused since line breaking is done via an array of |
| // line break offsets, and object boundary by finding no boundary within the |
| // current node. |
| case AXTextBoundary::kObject: |
| case AXTextBoundary::kLineStart: |
| case AXTextBoundary::kParagraphStart: |
| return base::i18n::BreakIterator::BREAK_NEWLINE; |
| default: |
| NOTREACHED() << boundary; |
| return base::i18n::BreakIterator::BREAK_NEWLINE; |
| } |
| } |
| |
| } // namespace |
| |
| // line_breaks is a Misnomer. Blink provides the start offsets of each line |
| // not the line breaks. |
| // TODO(nektar): Rename line_breaks a11y attribute and variable references. |
| size_t FindAccessibleTextBoundary(const base::string16& text, |
| const std::vector<int>& line_breaks, |
| AXTextBoundary boundary, |
| size_t start_offset, |
| TextBoundaryDirection direction, |
| ax::mojom::TextAffinity affinity) { |
| size_t text_size = text.size(); |
| DCHECK_LE(start_offset, text_size); |
| |
| base::i18n::BreakIterator::BreakType break_type = |
| ICUBreakTypeForBoundaryType(boundary); |
| base::i18n::BreakIterator break_iter(text, break_type); |
| if (boundary == AXTextBoundary::kCharacter || |
| boundary == AXTextBoundary::kSentenceStart || |
| boundary == AXTextBoundary::kWordStart || |
| boundary == AXTextBoundary::kWordStartOrEnd) { |
| if (!break_iter.Init()) |
| return start_offset; |
| } |
| |
| if (boundary == AXTextBoundary::kLineStart) { |
| if (direction == FORWARDS_DIRECTION) { |
| for (size_t j = 0; j < line_breaks.size(); ++j) { |
| size_t line_break = line_breaks[j] >= 0 ? line_breaks[j] : 0; |
| if ((affinity == ax::mojom::TextAffinity::kDownstream && |
| line_break > start_offset) || |
| (affinity == ax::mojom::TextAffinity::kUpstream && |
| line_break >= start_offset)) { |
| return line_break; |
| } |
| } |
| return text_size; |
| } else { |
| for (size_t j = line_breaks.size(); j != 0; --j) { |
| size_t line_break = line_breaks[j - 1] >= 0 ? line_breaks[j - 1] : 0; |
| if ((affinity == ax::mojom::TextAffinity::kDownstream && |
| line_break <= start_offset) || |
| (affinity == ax::mojom::TextAffinity::kUpstream && |
| line_break < start_offset)) { |
| return line_break; |
| } |
| } |
| return 0; |
| } |
| } |
| |
| size_t result = start_offset; |
| for (;;) { |
| size_t pos; |
| if (direction == FORWARDS_DIRECTION) { |
| if (result >= text_size) |
| return text_size; |
| pos = result; |
| } else { |
| if (result == 0) |
| return 0; |
| pos = result - 1; |
| } |
| |
| switch (boundary) { |
| case AXTextBoundary::kLineStart: |
| NOTREACHED() << boundary; // This is handled above. |
| return result; |
| case AXTextBoundary::kCharacter: |
| if (break_iter.IsGraphemeBoundary(result)) { |
| // If we are searching forward and we are still at the start offset, |
| // we need to find the next character. |
| if (direction == BACKWARDS_DIRECTION || result != start_offset) |
| return result; |
| } |
| break; |
| case AXTextBoundary::kWordStart: |
| if (break_iter.IsStartOfWord(result)) { |
| // If we are searching forward and we are still at the start offset, |
| // we need to find the next word. |
| if (direction == BACKWARDS_DIRECTION || result != start_offset) |
| return result; |
| } |
| break; |
| case AXTextBoundary::kWordStartOrEnd: |
| if (break_iter.IsStartOfWord(result)) { |
| // If we are searching forward and we are still at the start offset, |
| // we need to find the next word. |
| if (direction == BACKWARDS_DIRECTION || result != start_offset) |
| return result; |
| } else if (break_iter.IsEndOfWord(result)) { |
| // If we are searching backward and we are still at the end offset, we |
| // need to find the previous word. |
| if (direction == FORWARDS_DIRECTION || result != start_offset) |
| return result; |
| } |
| break; |
| case AXTextBoundary::kSentenceStart: |
| if (break_iter.IsSentenceBoundary(result)) { |
| // If we are searching forward and we are still at the start offset, |
| // we need to find the next sentence. |
| if (direction == BACKWARDS_DIRECTION || result != start_offset) { |
| // ICU sometimes returns sentence boundaries in the whitespace |
| // between sentences. For the purposes of accessibility, we want to |
| // include all whitespace at the end of a sentence. We move the |
| // boundary past the last whitespace offset. This works the same for |
| // backwards and forwards searches. |
| while (result < text_size && |
| base::IsUnicodeWhitespace(text[result])) |
| result++; |
| return result; |
| } |
| } |
| break; |
| case AXTextBoundary::kParagraphStart: |
| if (text[pos] == '\n') |
| return result; |
| break; |
| default: |
| break; |
| } |
| |
| if (direction == FORWARDS_DIRECTION) { |
| result++; |
| } else { |
| result--; |
| } |
| } |
| } |
| |
| base::string16 ActionVerbToLocalizedString( |
| const ax::mojom::DefaultActionVerb action_verb) { |
| switch (action_verb) { |
| case ax::mojom::DefaultActionVerb::kNone: |
| return base::string16(); |
| case ax::mojom::DefaultActionVerb::kActivate: |
| return l10n_util::GetStringUTF16(IDS_AX_ACTIVATE_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kCheck: |
| return l10n_util::GetStringUTF16(IDS_AX_CHECK_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kClick: |
| return l10n_util::GetStringUTF16(IDS_AX_CLICK_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kClickAncestor: |
| return l10n_util::GetStringUTF16(IDS_AX_CLICK_ANCESTOR_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kJump: |
| return l10n_util::GetStringUTF16(IDS_AX_JUMP_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kOpen: |
| return l10n_util::GetStringUTF16(IDS_AX_OPEN_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kPress: |
| return l10n_util::GetStringUTF16(IDS_AX_PRESS_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kSelect: |
| return l10n_util::GetStringUTF16(IDS_AX_SELECT_ACTION_VERB); |
| case ax::mojom::DefaultActionVerb::kUncheck: |
| return l10n_util::GetStringUTF16(IDS_AX_UNCHECK_ACTION_VERB); |
| } |
| NOTREACHED(); |
| return base::string16(); |
| } |
| |
| // Some APIs on Linux and Windows need to return non-localized action names. |
| base::string16 ActionVerbToUnlocalizedString( |
| const ax::mojom::DefaultActionVerb action_verb) { |
| switch (action_verb) { |
| case ax::mojom::DefaultActionVerb::kNone: |
| return base::UTF8ToUTF16("none"); |
| case ax::mojom::DefaultActionVerb::kActivate: |
| return base::UTF8ToUTF16("activate"); |
| case ax::mojom::DefaultActionVerb::kCheck: |
| return base::UTF8ToUTF16("check"); |
| case ax::mojom::DefaultActionVerb::kClick: |
| return base::UTF8ToUTF16("click"); |
| case ax::mojom::DefaultActionVerb::kClickAncestor: |
| return base::UTF8ToUTF16("click-ancestor"); |
| case ax::mojom::DefaultActionVerb::kJump: |
| return base::UTF8ToUTF16("jump"); |
| case ax::mojom::DefaultActionVerb::kOpen: |
| return base::UTF8ToUTF16("open"); |
| case ax::mojom::DefaultActionVerb::kPress: |
| return base::UTF8ToUTF16("press"); |
| case ax::mojom::DefaultActionVerb::kSelect: |
| return base::UTF8ToUTF16("select"); |
| case ax::mojom::DefaultActionVerb::kUncheck: |
| return base::UTF8ToUTF16("uncheck"); |
| } |
| NOTREACHED(); |
| return base::string16(); |
| } |
| |
| std::vector<int> GetWordStartOffsets(const base::string16& text) { |
| std::vector<int> word_starts; |
| base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); |
| if (!iter.Init()) |
| return word_starts; |
| // iter.Advance() returns false if we've run past end of the text. |
| while (iter.Advance()) { |
| if (!iter.IsWord()) |
| continue; |
| word_starts.push_back( |
| base::checked_cast<int>(iter.prev()) /* start index */); |
| } |
| return word_starts; |
| } |
| |
| std::vector<int> GetWordEndOffsets(const base::string16& text) { |
| std::vector<int> word_ends; |
| base::i18n::BreakIterator iter(text, base::i18n::BreakIterator::BREAK_WORD); |
| if (!iter.Init()) |
| return word_ends; |
| // iter.Advance() returns false if we've run past end of the text. |
| while (iter.Advance()) { |
| if (!iter.IsWord()) |
| continue; |
| word_ends.push_back(base::checked_cast<int>(iter.pos()) /* end index */); |
| } |
| return word_ends; |
| } |
| |
| } // namespace ui |