1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozInlineSpellWordUtil.h"
11 #include "mozilla/BinarySearch.h"
12 #include "mozilla/HTMLEditor.h"
13 #include "mozilla/Logging.h"
14 #include "mozilla/TextEditor.h"
15 #include "mozilla/dom/Element.h"
19 #include "nsComponentManagerUtils.h"
20 #include "nsUnicodeProperties.h"
21 #include "nsServiceManagerUtils.h"
22 #include "nsIContent.h"
23 #include "nsTextFragment.h"
25 #include "nsContentUtils.h"
28 using namespace mozilla
;
30 static LazyLogModule sInlineSpellWordUtilLog
{"InlineSpellWordUtil"};
32 // IsIgnorableCharacter
34 // These characters are ones that we should ignore in input.
36 inline bool IsIgnorableCharacter(char ch
) {
37 return (ch
== static_cast<char>(0xAD)); // SOFT HYPHEN
40 inline bool IsIgnorableCharacter(char16_t ch
) {
41 return (ch
== 0xAD || // SOFT HYPHEN
42 ch
== 0x1806); // MONGOLIAN TODO SOFT HYPHEN
45 // IsConditionalPunctuation
47 // Some characters (like apostrophes) require characters on each side to be
48 // part of a word, and are otherwise punctuation.
50 inline bool IsConditionalPunctuation(char ch
) {
51 return (ch
== '\'' || // RIGHT SINGLE QUOTATION MARK
52 ch
== static_cast<char>(0xB7)); // MIDDLE DOT
55 inline bool IsConditionalPunctuation(char16_t ch
) {
56 return (ch
== '\'' || ch
== 0x2019 || // RIGHT SINGLE QUOTATION MARK
57 ch
== 0x00B7); // MIDDLE DOT
60 static bool IsAmbiguousDOMWordSeprator(char16_t ch
) {
61 // This class may be CHAR_CLASS_SEPARATOR, but it depends on context.
62 return (ch
== '@' || ch
== ':' || ch
== '.' || ch
== '/' || ch
== '-' ||
63 IsConditionalPunctuation(ch
));
66 static bool IsAmbiguousDOMWordSeprator(char ch
) {
67 // This class may be CHAR_CLASS_SEPARATOR, but it depends on context.
68 return IsAmbiguousDOMWordSeprator(static_cast<char16_t
>(ch
));
73 // Determines if the given character should be considered as a DOM Word
74 // separator. Basically, this is whitespace, although it could also have
75 // certain punctuation that we know ALWAYS breaks words. This is important.
76 // For example, we can't have any punctuation that could appear in a URL
77 // or email address in this, because those need to always fit into a single
80 static bool IsDOMWordSeparator(char ch
) {
81 // simple spaces or no-break space
82 return (ch
== ' ' || ch
== '\t' || ch
== '\n' || ch
== '\r' ||
83 ch
== static_cast<char>(0xA0));
86 static bool IsDOMWordSeparator(char16_t ch
) {
88 if (ch
== ' ' || ch
== '\t' || ch
== '\n' || ch
== '\r') return true;
90 // complex spaces - check only if char isn't ASCII (uncommon)
91 if (ch
>= 0xA0 && (ch
== 0x00A0 || // NO-BREAK SPACE
92 ch
== 0x2002 || // EN SPACE
93 ch
== 0x2003 || // EM SPACE
94 ch
== 0x2009 || // THIN SPACE
95 ch
== 0x3000)) // IDEOGRAPHIC SPACE
98 // otherwise not a space
103 Maybe
<mozInlineSpellWordUtil
> mozInlineSpellWordUtil::Create(
104 const TextEditor
& aTextEditor
) {
105 dom::Document
* document
= aTextEditor
.GetDocument();
106 if (NS_WARN_IF(!document
)) {
110 const bool isContentEditableOrDesignMode
= !!aTextEditor
.AsHTMLEditor();
112 // Find the root node for the editor. For contenteditable the mRootNode could
113 // change to shadow root if the begin and end are inside the shadowDOM.
114 nsINode
* rootNode
= aTextEditor
.GetRoot();
115 if (NS_WARN_IF(!rootNode
)) {
119 mozInlineSpellWordUtil util
{*document
, isContentEditableOrDesignMode
,
121 return Some(std::move(util
));
124 static inline bool IsSpellCheckingTextNode(nsINode
* aNode
) {
125 nsIContent
* parent
= aNode
->GetParent();
127 parent
->IsAnyOfHTMLElements(nsGkAtoms::script
, nsGkAtoms::style
))
129 return aNode
->IsText();
132 typedef void (*OnLeaveNodeFunPtr
)(nsINode
* aNode
, void* aClosure
);
134 // Find the next node in the DOM tree in preorder.
135 // Calls OnLeaveNodeFunPtr when the traversal leaves a node, which is
136 // why we can't just use GetNextNode here, sadly.
137 static nsINode
* FindNextNode(nsINode
* aNode
, const nsINode
* aRoot
,
138 OnLeaveNodeFunPtr aOnLeaveNode
, void* aClosure
) {
139 MOZ_ASSERT(aNode
, "Null starting node?");
141 nsINode
* next
= aNode
->GetFirstChild();
142 if (next
) return next
;
144 // Don't look at siblings or otherwise outside of aRoot
145 if (aNode
== aRoot
) return nullptr;
147 next
= aNode
->GetNextSibling();
148 if (next
) return next
;
153 aOnLeaveNode(aNode
, aClosure
);
156 next
= aNode
->GetParent();
157 if (next
== aRoot
|| !next
) return nullptr;
160 next
= aNode
->GetNextSibling();
161 if (next
) return next
;
165 // aNode is not a text node. Find the first text node starting at aNode/aOffset
166 // in a preorder DOM traversal.
167 static nsINode
* FindNextTextNode(nsINode
* aNode
, int32_t aOffset
,
168 const nsINode
* aRoot
) {
169 MOZ_ASSERT(aNode
, "Null starting node?");
170 NS_ASSERTION(!IsSpellCheckingTextNode(aNode
),
171 "FindNextTextNode should start with a non-text node");
174 // Need to start at the aOffset'th child
175 nsIContent
* child
= aNode
->GetChildAt_Deprecated(aOffset
);
180 // aOffset was beyond the end of the child list.
181 // goto next node after the last descendant of aNode in
182 // a preorder DOM traversal.
183 checkNode
= aNode
->GetNextNonChildNode(aRoot
);
186 while (checkNode
&& !IsSpellCheckingTextNode(checkNode
)) {
187 checkNode
= checkNode
->GetNextNode(aRoot
);
192 // mozInlineSpellWordUtil::SetPositionAndEnd
194 // We have two ranges "hard" and "soft". The hard boundary is simply
195 // the scope of the root node. The soft boundary is that which is set
196 // by the caller of this class by calling this function. If this function is
197 // not called, the soft boundary is the same as the hard boundary.
199 // When we reach the soft boundary (mSoftEnd), we keep
200 // going until we reach the end of a word. This allows the caller to set the
201 // end of the range to anything, and we will always check whole multiples of
202 // words. When we reach the hard boundary we stop no matter what.
204 // There is no beginning soft boundary. This is because we only go to the
205 // previous node once, when finding the previous word boundary in
206 // SetPosition(). You might think of the soft boundary as being this initial
209 nsresult
mozInlineSpellWordUtil::SetPositionAndEnd(nsINode
* aPositionNode
,
210 int32_t aPositionOffset
,
212 int32_t aEndOffset
) {
213 MOZ_LOG(sInlineSpellWordUtilLog
, LogLevel::Debug
,
214 ("%s: pos=(%p, %i), end=(%p, %i)", __FUNCTION__
, aPositionNode
,
215 aPositionOffset
, aEndNode
, aEndOffset
));
217 MOZ_ASSERT(aPositionNode
, "Null begin node?");
218 MOZ_ASSERT(aEndNode
, "Null end node?");
220 NS_ASSERTION(mRootNode
, "Not initialized");
222 // Find a appropriate root if we are dealing with contenteditable nodes which
223 // are in the shadow DOM.
224 if (mIsContentEditableOrDesignMode
) {
225 nsINode
* rootNode
= aPositionNode
->SubtreeRoot();
226 if (rootNode
!= aEndNode
->SubtreeRoot()) {
227 return NS_ERROR_FAILURE
;
230 if (mozilla::dom::ShadowRoot::FromNode(rootNode
)) {
231 mRootNode
= rootNode
;
237 if (!IsSpellCheckingTextNode(aPositionNode
)) {
238 // Start at the start of the first text node after aNode/aOffset.
239 aPositionNode
= FindNextTextNode(aPositionNode
, aPositionOffset
, mRootNode
);
242 mSoftBegin
= NodeOffset(aPositionNode
, aPositionOffset
);
244 if (!IsSpellCheckingTextNode(aEndNode
)) {
245 // End at the start of the first text node after aEndNode/aEndOffset.
246 aEndNode
= FindNextTextNode(aEndNode
, aEndOffset
, mRootNode
);
249 mSoftEnd
= NodeOffset(aEndNode
, aEndOffset
);
251 nsresult rv
= EnsureWords();
256 int32_t textOffset
= MapDOMPositionToSoftTextOffset(mSoftBegin
);
257 if (textOffset
< 0) {
261 mNextWordIndex
= FindRealWordContaining(textOffset
, HINT_END
, true);
265 nsresult
mozInlineSpellWordUtil::EnsureWords() {
266 if (mSoftTextValid
) return NS_OK
;
270 Result
<RealWords
, nsresult
> realWords
= BuildRealWords();
271 if (realWords
.isErr()) {
272 return realWords
.unwrapErr();
275 mRealWords
= realWords
.unwrap();
276 mSoftTextValid
= true;
280 nsresult
mozInlineSpellWordUtil::MakeRangeForWord(const RealWord
& aWord
,
281 nsRange
** aRange
) const {
283 MapSoftTextOffsetToDOMPosition(aWord
.mSoftTextOffset
, HINT_BEGIN
);
284 NodeOffset end
= MapSoftTextOffsetToDOMPosition(aWord
.EndOffset(), HINT_END
);
285 return MakeRange(begin
, end
, aRange
);
287 void mozInlineSpellWordUtil::MakeNodeOffsetRangeForWord(
288 const RealWord
& aWord
, NodeOffsetRange
* aNodeOffsetRange
) {
290 MapSoftTextOffsetToDOMPosition(aWord
.mSoftTextOffset
, HINT_BEGIN
);
291 NodeOffset end
= MapSoftTextOffsetToDOMPosition(aWord
.EndOffset(), HINT_END
);
292 *aNodeOffsetRange
= NodeOffsetRange(begin
, end
);
295 // mozInlineSpellWordUtil::GetRangeForWord
297 nsresult
mozInlineSpellWordUtil::GetRangeForWord(nsINode
* aWordNode
,
300 // Set our soft end and start
301 NodeOffset
pt(aWordNode
, aWordOffset
);
303 if (!mSoftTextValid
|| pt
!= mSoftBegin
|| pt
!= mSoftEnd
) {
305 mSoftBegin
= mSoftEnd
= pt
;
306 nsresult rv
= EnsureWords();
312 int32_t offset
= MapDOMPositionToSoftTextOffset(pt
);
313 if (offset
< 0) return MakeRange(pt
, pt
, aRange
);
314 int32_t wordIndex
= FindRealWordContaining(offset
, HINT_BEGIN
, false);
315 if (wordIndex
< 0) return MakeRange(pt
, pt
, aRange
);
316 return MakeRangeForWord(mRealWords
[wordIndex
], aRange
);
319 // This is to fix characters that the spellchecker may not like
320 static void NormalizeWord(const nsAString
& aInput
, int32_t aPos
, int32_t aLen
,
321 nsAString
& aOutput
) {
323 for (int32_t i
= 0; i
< aLen
; i
++) {
324 char16_t ch
= aInput
.CharAt(i
+ aPos
);
326 // remove ignorable characters from the word
327 if (IsIgnorableCharacter(ch
)) continue;
329 // the spellchecker doesn't handle curly apostrophes in all languages
330 if (ch
== 0x2019) { // RIGHT SINGLE QUOTATION MARK
338 // mozInlineSpellWordUtil::GetNextWord
340 // FIXME-optimization: we shouldn't have to generate a range every single
341 // time. It would be better if the inline spellchecker didn't require a
342 // range unless the word was misspelled. This may or may not be possible.
344 bool mozInlineSpellWordUtil::GetNextWord(nsAString
& aText
,
345 NodeOffsetRange
* aNodeOffsetRange
,
346 bool* aSkipChecking
) {
347 MOZ_LOG(sInlineSpellWordUtilLog
, LogLevel::Debug
,
348 ("%s: mNextWordIndex=%d", __FUNCTION__
, mNextWordIndex
));
350 if (mNextWordIndex
< 0 || mNextWordIndex
>= int32_t(mRealWords
.Length())) {
352 *aSkipChecking
= true;
356 const RealWord
& word
= mRealWords
[mNextWordIndex
];
357 MakeNodeOffsetRangeForWord(word
, aNodeOffsetRange
);
359 *aSkipChecking
= !word
.mCheckableWord
;
360 ::NormalizeWord(mSoftText
, word
.mSoftTextOffset
, word
.mLength
, aText
);
362 MOZ_LOG(sInlineSpellWordUtilLog
, LogLevel::Debug
,
363 ("%s: returning: %s (skip=%d)", __FUNCTION__
,
364 NS_ConvertUTF16toUTF8(aText
).get(), *aSkipChecking
));
369 // mozInlineSpellWordUtil::MakeRange
371 // Convenience function for creating a range over the current document.
373 nsresult
mozInlineSpellWordUtil::MakeRange(NodeOffset aBegin
, NodeOffset aEnd
,
374 nsRange
** aRange
) const {
375 NS_ENSURE_ARG_POINTER(aBegin
.mNode
);
377 return NS_ERROR_NOT_INITIALIZED
;
381 RefPtr
<nsRange
> range
= nsRange::Create(aBegin
.mNode
, aBegin
.mOffset
,
382 aEnd
.mNode
, aEnd
.mOffset
, error
);
383 if (NS_WARN_IF(error
.Failed())) {
384 return error
.StealNSResult();
387 range
.forget(aRange
);
392 already_AddRefed
<nsRange
> mozInlineSpellWordUtil::MakeRange(
393 const NodeOffsetRange
& aRange
) {
394 IgnoredErrorResult ignoredError
;
395 RefPtr
<nsRange
> range
=
396 nsRange::Create(aRange
.Begin().Node(), aRange
.Begin().Offset(),
397 aRange
.End().Node(), aRange
.End().Offset(), ignoredError
);
398 NS_WARNING_ASSERTION(!ignoredError
.Failed(), "Creating a range failed");
399 return range
.forget();
402 /*********** Word Splitting ************/
404 // classifies a given character in the DOM word
407 CHAR_CLASS_SEPARATOR
,
408 CHAR_CLASS_END_OF_INPUT
411 // Encapsulates DOM-word to real-word splitting
413 struct MOZ_STACK_CLASS WordSplitState
{
414 const T
& mDOMWordText
;
415 int32_t mDOMWordOffset
;
416 CharClass mCurCharClass
;
418 explicit WordSplitState(const T
& aString
)
419 : mDOMWordText(aString
),
421 mCurCharClass(CHAR_CLASS_END_OF_INPUT
) {}
423 CharClass
ClassifyCharacter(int32_t aIndex
, bool aRecurse
) const;
425 void AdvanceThroughSeparators();
426 void AdvanceThroughWord();
428 // Finds special words like email addresses and URLs that may start at the
429 // current position, and returns their length, or 0 if not found. This allows
430 // arbitrary word breaking rules to be used for these special entities, as
431 // long as they can not contain whitespace.
432 bool IsSpecialWord() const;
434 // Similar to IsSpecialWord except that this takes a split word as
435 // input. This checks for things that do not require special word-breaking
437 bool ShouldSkipWord(int32_t aStart
, int32_t aLength
) const;
439 // Checks to see if there's a DOM word separator before aBeforeOffset within
440 // it. This function does not modify aSeparatorOffset when it returns false.
441 bool GetDOMWordSeparatorOffset(int32_t aOffset
,
442 int32_t* aSeparatorOffset
) const;
444 char16_t
GetUnicharAt(int32_t aIndex
) const;
447 // WordSplitState::ClassifyCharacter
449 CharClass WordSplitState
<T
>::ClassifyCharacter(int32_t aIndex
,
450 bool aRecurse
) const {
451 NS_ASSERTION(aIndex
>= 0 && aIndex
<= int32_t(mDOMWordText
.Length()),
452 "Index out of range");
453 if (aIndex
== int32_t(mDOMWordText
.Length())) return CHAR_CLASS_SEPARATOR
;
455 // this will classify the character, we want to treat "ignorable" characters
456 // such as soft hyphens, and also ZWJ and ZWNJ as word characters.
457 nsUGenCategory charCategory
=
458 mozilla::unicode::GetGenCategory(GetUnicharAt(aIndex
));
459 if (charCategory
== nsUGenCategory::kLetter
||
460 IsIgnorableCharacter(mDOMWordText
[aIndex
]) ||
461 mDOMWordText
[aIndex
] == 0x200C /* ZWNJ */ ||
462 mDOMWordText
[aIndex
] == 0x200D /* ZWJ */)
463 return CHAR_CLASS_WORD
;
465 // If conditional punctuation is surrounded immediately on both sides by word
466 // characters it also counts as a word character.
467 if (IsConditionalPunctuation(mDOMWordText
[aIndex
])) {
469 // not allowed to look around, this punctuation counts like a separator
470 return CHAR_CLASS_SEPARATOR
;
473 // check the left-hand character
474 if (aIndex
== 0) return CHAR_CLASS_SEPARATOR
;
475 if (ClassifyCharacter(aIndex
- 1, false) != CHAR_CLASS_WORD
)
476 return CHAR_CLASS_SEPARATOR
;
477 // If the previous charatcer is a word-char, make sure that it's not a
478 // special dot character.
479 if (mDOMWordText
[aIndex
- 1] == '.') return CHAR_CLASS_SEPARATOR
;
481 // now we know left char is a word-char, check the right-hand character
482 if (aIndex
== int32_t(mDOMWordText
.Length() - 1)) {
483 return CHAR_CLASS_SEPARATOR
;
486 if (ClassifyCharacter(aIndex
+ 1, false) != CHAR_CLASS_WORD
)
487 return CHAR_CLASS_SEPARATOR
;
488 // If the next charatcer is a word-char, make sure that it's not a
489 // special dot character.
490 if (mDOMWordText
[aIndex
+ 1] == '.') return CHAR_CLASS_SEPARATOR
;
492 // char on either side is a word, this counts as a word
493 return CHAR_CLASS_WORD
;
496 // The dot character, if appearing at the end of a word, should
497 // be considered part of that word. Example: "etc.", or
499 if (aIndex
> 0 && mDOMWordText
[aIndex
] == '.' &&
500 mDOMWordText
[aIndex
- 1] != '.' &&
501 ClassifyCharacter(aIndex
- 1, false) != CHAR_CLASS_WORD
) {
502 return CHAR_CLASS_WORD
;
505 // all other punctuation
506 if (charCategory
== nsUGenCategory::kSeparator
||
507 charCategory
== nsUGenCategory::kOther
||
508 charCategory
== nsUGenCategory::kPunctuation
||
509 charCategory
== nsUGenCategory::kSymbol
) {
510 // Don't break on hyphens, as hunspell handles them on its own.
511 if (aIndex
> 0 && mDOMWordText
[aIndex
] == '-' &&
512 mDOMWordText
[aIndex
- 1] != '-' &&
513 ClassifyCharacter(aIndex
- 1, false) == CHAR_CLASS_WORD
) {
514 // A hyphen is only meaningful as a separator inside a word
515 // if the previous and next characters are a word character.
516 if (aIndex
== int32_t(mDOMWordText
.Length()) - 1)
517 return CHAR_CLASS_SEPARATOR
;
518 if (mDOMWordText
[aIndex
+ 1] != '.' &&
519 ClassifyCharacter(aIndex
+ 1, false) == CHAR_CLASS_WORD
)
520 return CHAR_CLASS_WORD
;
522 return CHAR_CLASS_SEPARATOR
;
525 // any other character counts as a word
526 return CHAR_CLASS_WORD
;
529 // WordSplitState::Advance
531 void WordSplitState
<T
>::Advance() {
532 NS_ASSERTION(mDOMWordOffset
>= 0, "Negative word index");
533 NS_ASSERTION(mDOMWordOffset
< (int32_t)mDOMWordText
.Length(),
534 "Length beyond end");
537 if (mDOMWordOffset
>= (int32_t)mDOMWordText
.Length())
538 mCurCharClass
= CHAR_CLASS_END_OF_INPUT
;
540 mCurCharClass
= ClassifyCharacter(mDOMWordOffset
, true);
543 // WordSplitState::AdvanceThroughSeparators
545 void WordSplitState
<T
>::AdvanceThroughSeparators() {
546 while (mCurCharClass
== CHAR_CLASS_SEPARATOR
) Advance();
549 // WordSplitState::AdvanceThroughWord
551 void WordSplitState
<T
>::AdvanceThroughWord() {
552 while (mCurCharClass
== CHAR_CLASS_WORD
) Advance();
555 // WordSplitState::IsSpecialWord
557 bool WordSplitState
<T
>::IsSpecialWord() const {
558 // Search for email addresses. We simply define these as any sequence of
559 // characters with an '@' character in the middle. The DOM word is already
560 // split on whitepace, so we know that everything to the end is the address
561 int32_t firstColon
= -1;
562 for (int32_t i
= mDOMWordOffset
; i
< int32_t(mDOMWordText
.Length()); i
++) {
563 if (mDOMWordText
[i
] == '@') {
564 // only accept this if there are unambiguous word characters (don't bother
565 // recursing to disambiguate apostrophes) on each side. This prevents
566 // classifying, e.g. "@home" as an email address
568 // Use this condition to only accept words with '@' in the middle of
569 // them. It works, but the inlinespellcker doesn't like this. The problem
570 // is that you type "fhsgfh@" that's a misspelled word followed by a
571 // symbol, but when you type another letter "fhsgfh@g" that first word
572 // need to be unmarked misspelled. It doesn't do this. it only checks the
573 // current position for potentially removing a spelling range.
574 if (i
> 0 && ClassifyCharacter(i
- 1, false) == CHAR_CLASS_WORD
&&
575 i
< (int32_t)mDOMWordText
.Length() - 1 &&
576 ClassifyCharacter(i
+ 1, false) == CHAR_CLASS_WORD
) {
579 } else if (mDOMWordText
[i
] == ':' && firstColon
< 0) {
582 // If the first colon is followed by a slash, consider it a URL
583 // This will catch things like asdf://foo.com
584 if (firstColon
< (int32_t)mDOMWordText
.Length() - 1 &&
585 mDOMWordText
[firstColon
+ 1] == '/') {
591 // Check the text before the first colon against some known protocols. It
592 // is impossible to check against all protocols, especially since you can
593 // plug in new protocols. We also don't want to waste time here checking
594 // against a lot of obscure protocols.
595 if (firstColon
> mDOMWordOffset
) {
597 Substring(mDOMWordText
, mDOMWordOffset
, firstColon
- mDOMWordOffset
));
598 if (protocol
.EqualsIgnoreCase("http") ||
599 protocol
.EqualsIgnoreCase("https") ||
600 protocol
.EqualsIgnoreCase("news") ||
601 protocol
.EqualsIgnoreCase("file") ||
602 protocol
.EqualsIgnoreCase("javascript") ||
603 protocol
.EqualsIgnoreCase("data") || protocol
.EqualsIgnoreCase("ftp")) {
608 // not anything special
612 // WordSplitState::ShouldSkipWord
614 bool WordSplitState
<T
>::ShouldSkipWord(int32_t aStart
, int32_t aLength
) const {
615 int32_t last
= aStart
+ aLength
;
617 // check to see if the word contains a digit
618 for (int32_t i
= aStart
; i
< last
; i
++) {
619 if (mozilla::unicode::GetGenCategory(GetUnicharAt(i
)) ==
620 nsUGenCategory::kNumber
) {
630 bool WordSplitState
<T
>::GetDOMWordSeparatorOffset(
631 int32_t aOffset
, int32_t* aSeparatorOffset
) const {
632 for (int32_t i
= aOffset
- 1; i
>= 0; --i
) {
633 if (IsDOMWordSeparator(mDOMWordText
[i
]) ||
634 (!IsAmbiguousDOMWordSeprator(mDOMWordText
[i
]) &&
635 ClassifyCharacter(i
, true) == CHAR_CLASS_SEPARATOR
)) {
636 // Be greedy, find as many separators as we can
637 for (int32_t j
= i
- 1; j
>= 0; --j
) {
638 if (IsDOMWordSeparator(mDOMWordText
[j
]) ||
639 (!IsAmbiguousDOMWordSeprator(mDOMWordText
[j
]) &&
640 ClassifyCharacter(j
, true) == CHAR_CLASS_SEPARATOR
)) {
646 *aSeparatorOffset
= i
;
654 char16_t WordSplitState
<nsDependentSubstring
>::GetUnicharAt(
655 int32_t aIndex
) const {
656 return mDOMWordText
[aIndex
];
660 char16_t WordSplitState
<nsDependentCSubstring
>::GetUnicharAt(
661 int32_t aIndex
) const {
662 return static_cast<char16_t
>(static_cast<uint8_t>(mDOMWordText
[aIndex
]));
665 static inline bool IsBRElement(nsINode
* aNode
) {
666 return aNode
->IsHTMLElement(nsGkAtoms::br
);
670 * Given a TextNode, checks to see if there's a DOM word separator before
671 * aBeforeOffset within it. This function does not modify aSeparatorOffset when
674 * @param aContent the TextNode to check.
675 * @param aBeforeOffset the offset in the TextNode before which we will search
676 * for the DOM separator. You can pass INT32_MAX to search the entire
677 * length of the string.
678 * @param aSeparatorOffset will be set to the offset of the first separator it
679 * encounters. Will not be written to if no separator is found.
680 * @returns True if it found a separator.
682 static bool TextNodeContainsDOMWordSeparator(nsIContent
* aContent
,
683 int32_t aBeforeOffset
,
684 int32_t* aSeparatorOffset
) {
685 const nsTextFragment
* textFragment
= aContent
->GetText();
686 NS_ASSERTION(textFragment
, "Where is our text?");
687 int32_t end
= std::min(aBeforeOffset
, int32_t(textFragment
->GetLength()));
689 if (textFragment
->Is2b()) {
690 nsDependentSubstring
targetText(textFragment
->Get2b(), end
);
691 WordSplitState
<nsDependentSubstring
> state(targetText
);
692 return state
.GetDOMWordSeparatorOffset(end
, aSeparatorOffset
);
695 nsDependentCSubstring
targetText(textFragment
->Get1b(), end
);
696 WordSplitState
<nsDependentCSubstring
> state(targetText
);
697 return state
.GetDOMWordSeparatorOffset(end
, aSeparatorOffset
);
701 * Check if there's a DOM word separator before aBeforeOffset in this node.
702 * Always returns true if it's a BR element.
703 * aSeparatorOffset is set to the index of the first character in the last
704 * separator if any is found (0 for BR elements).
706 * This function does not modify aSeparatorOffset when it returns false.
708 static bool ContainsDOMWordSeparator(nsINode
* aNode
, int32_t aBeforeOffset
,
709 int32_t* aSeparatorOffset
) {
710 if (IsBRElement(aNode
)) {
711 *aSeparatorOffset
= 0;
715 if (!IsSpellCheckingTextNode(aNode
)) return false;
717 return TextNodeContainsDOMWordSeparator(aNode
->AsContent(), aBeforeOffset
,
721 static bool IsBreakElement(nsINode
* aNode
) {
722 if (!aNode
->IsElement()) {
726 dom::Element
* element
= aNode
->AsElement();
727 if (element
->IsHTMLElement(nsGkAtoms::br
)) {
731 // If we don't have a frame, we don't consider ourselves a break
732 // element. In particular, words can span us.
733 nsIFrame
* frame
= element
->GetPrimaryFrame();
738 auto* disp
= frame
->StyleDisplay();
739 // Anything that's not an inline element is a break element.
740 // XXXbz should replaced inlines be break elements, though?
741 // Also should inline-block and such be break elements?
743 // FIXME(emilio): We should teach the spell checker to deal with generated
744 // content (it doesn't at all), then remove the IsListItem() check, as there
745 // could be no marker, etc...
746 return !disp
->IsInlineFlow() || disp
->IsListItem();
749 struct CheckLeavingBreakElementClosure
{
750 bool mLeftBreakElement
;
753 static void CheckLeavingBreakElement(nsINode
* aNode
, void* aClosure
) {
754 CheckLeavingBreakElementClosure
* cl
=
755 static_cast<CheckLeavingBreakElementClosure
*>(aClosure
);
756 if (!cl
->mLeftBreakElement
&& IsBreakElement(aNode
)) {
757 cl
->mLeftBreakElement
= true;
761 void mozInlineSpellWordUtil::NormalizeWord(nsAString
& aWord
) {
763 ::NormalizeWord(aWord
, 0, aWord
.Length(), result
);
767 void mozInlineSpellWordUtil::BuildSoftText() {
768 MOZ_LOG(sInlineSpellWordUtilLog
, LogLevel::Debug
, ("%s", __FUNCTION__
));
770 // First we have to work backwards from mSoftStart to find a text node
771 // containing a DOM word separator, a non-inline-element
772 // boundary, or the hard start node. That's where we'll start building the
774 nsINode
* node
= mSoftBegin
.mNode
;
775 int32_t firstOffsetInNode
= 0;
776 int32_t checkBeforeOffset
= mSoftBegin
.mOffset
;
778 if (ContainsDOMWordSeparator(node
, checkBeforeOffset
, &firstOffsetInNode
)) {
779 if (node
== mSoftBegin
.mNode
) {
780 // If we find a word separator on the first node, look at the preceding
781 // word on the text node as well.
782 int32_t newOffset
= 0;
783 if (firstOffsetInNode
> 0) {
784 // Try to find the previous word boundary in the current node. If
785 // we can't find one, start checking previous sibling nodes (if any
786 // adjacent ones exist) to see if we can find any text nodes with
787 // DOM word separators. We bail out as soon as we see a node that is
788 // not a text node, or we run out of previous sibling nodes. In the
789 // event that we simply cannot find any preceding word separator, the
790 // offset is set to 0, and the soft text beginning node is set to the
791 // "most previous" text node before the original starting node, or
792 // kept at the original starting node if no previous text nodes exist.
793 if (!ContainsDOMWordSeparator(node
, firstOffsetInNode
- 1,
795 nsIContent
* prevNode
= node
->GetPreviousSibling();
796 while (prevNode
&& IsSpellCheckingTextNode(prevNode
)) {
797 mSoftBegin
.mNode
= prevNode
;
798 if (TextNodeContainsDOMWordSeparator(prevNode
, INT32_MAX
,
802 prevNode
= prevNode
->GetPreviousSibling();
806 firstOffsetInNode
= newOffset
;
807 mSoftBegin
.mOffset
= newOffset
;
811 checkBeforeOffset
= INT32_MAX
;
812 if (IsBreakElement(node
)) {
813 // Since GetPreviousContent follows tree *preorder*, we're about to
814 // traverse up out of 'node'. Since node induces breaks (e.g., it's a
815 // block), don't bother trying to look outside it, just stop now.
818 // GetPreviousContent below expects mRootNode to be an ancestor of node.
819 if (!node
->IsInclusiveDescendantOf(mRootNode
)) {
822 node
= node
->GetPreviousContent(mRootNode
);
825 // Now build up the string moving forward through the DOM until we reach
826 // the soft end and *then* see a DOM word separator, a non-inline-element
827 // boundary, or the hard end node.
828 mSoftText
.Truncate();
829 mSoftTextDOMMapping
.Clear();
830 bool seenSoftEnd
= false;
831 // Leave this outside the loop so large heap string allocations can be reused
834 if (node
== mSoftEnd
.mNode
) {
839 if (IsSpellCheckingTextNode(node
)) {
840 nsIContent
* content
= static_cast<nsIContent
*>(node
);
841 NS_ASSERTION(content
, "Where is our content?");
842 const nsTextFragment
* textFragment
= content
->GetText();
843 NS_ASSERTION(textFragment
, "Where is our text?");
844 int32_t lastOffsetInNode
= textFragment
->GetLength();
847 // check whether we can stop after this
848 for (int32_t i
= node
== mSoftEnd
.mNode
? mSoftEnd
.mOffset
: 0;
849 i
< int32_t(textFragment
->GetLength()); ++i
) {
850 if (IsDOMWordSeparator(textFragment
->CharAt(i
))) {
852 // stop at the first separator after the soft end point
853 lastOffsetInNode
= i
;
859 if (firstOffsetInNode
< lastOffsetInNode
) {
860 int32_t len
= lastOffsetInNode
- firstOffsetInNode
;
861 mSoftTextDOMMapping
.AppendElement(DOMTextMapping(
862 NodeOffset(node
, firstOffsetInNode
), mSoftText
.Length(), len
));
864 bool ok
= textFragment
->AppendTo(mSoftText
, firstOffsetInNode
, len
,
867 // probably out of memory, remove from mSoftTextDOMMapping
868 mSoftTextDOMMapping
.RemoveLastElement();
873 firstOffsetInNode
= 0;
878 CheckLeavingBreakElementClosure closure
= {false};
879 node
= FindNextNode(node
, mRootNode
, CheckLeavingBreakElement
, &closure
);
880 if (closure
.mLeftBreakElement
|| (node
&& IsBreakElement(node
))) {
881 // We left, or are entering, a break element (e.g., block). Maybe we can
883 if (seenSoftEnd
) break;
885 mSoftText
.Append(' ');
889 MOZ_LOG(sInlineSpellWordUtilLog
, LogLevel::Debug
,
890 ("%s: got DOM string: %s", __FUNCTION__
,
891 NS_ConvertUTF16toUTF8(mSoftText
).get()));
894 auto mozInlineSpellWordUtil::BuildRealWords() const
895 -> Result
<RealWords
, nsresult
> {
896 // This is pretty simple. We just have to walk mSoftText, tokenizing it
897 // into "real words".
898 // We do an outer traversal of words delimited by IsDOMWordSeparator, calling
899 // SplitDOMWordAndAppendTo on each of those DOM words
900 int32_t wordStart
= -1;
902 for (int32_t i
= 0; i
< int32_t(mSoftText
.Length()); ++i
) {
903 if (IsDOMWordSeparator(mSoftText
.CharAt(i
))) {
904 if (wordStart
>= 0) {
905 nsresult rv
= SplitDOMWordAndAppendTo(wordStart
, i
, realWords
);
917 if (wordStart
>= 0) {
919 SplitDOMWordAndAppendTo(wordStart
, mSoftText
.Length(), realWords
);
928 /*********** DOM/realwords<->mSoftText mapping functions ************/
930 int32_t mozInlineSpellWordUtil::MapDOMPositionToSoftTextOffset(
931 NodeOffset aNodeOffset
) const {
932 if (!mSoftTextValid
) {
933 NS_ERROR("Soft text must be valid if we're to map into it");
937 for (int32_t i
= 0; i
< int32_t(mSoftTextDOMMapping
.Length()); ++i
) {
938 const DOMTextMapping
& map
= mSoftTextDOMMapping
[i
];
939 if (map
.mNodeOffset
.mNode
== aNodeOffset
.mNode
) {
940 // Allow offsets at either end of the string, in particular, allow the
941 // offset that's at the end of the contributed string
942 int32_t offsetInContributedString
=
943 aNodeOffset
.mOffset
- map
.mNodeOffset
.mOffset
;
944 if (offsetInContributedString
>= 0 &&
945 offsetInContributedString
<= map
.mLength
)
946 return map
.mSoftTextOffset
+ offsetInContributedString
;
956 class FirstLargerOffset
{
957 int32_t mSoftTextOffset
;
960 explicit FirstLargerOffset(int32_t aSoftTextOffset
)
961 : mSoftTextOffset(aSoftTextOffset
) {}
962 int operator()(const T
& t
) const {
963 // We want the first larger offset, so never return 0 (which would
964 // short-circuit evaluation before finding the last such offset).
965 return mSoftTextOffset
< t
.mSoftTextOffset
? -1 : 1;
970 bool FindLastNongreaterOffset(const nsTArray
<T
>& aContainer
,
971 int32_t aSoftTextOffset
, size_t* aIndex
) {
972 if (aContainer
.Length() == 0) {
976 BinarySearchIf(aContainer
, 0, aContainer
.Length(),
977 FirstLargerOffset
<T
>(aSoftTextOffset
), aIndex
);
979 // There was at least one mapping with offset <= aSoftTextOffset. Step back
980 // to find the last element with |mSoftTextOffset <= aSoftTextOffset|.
983 // Every mapping had offset greater than aSoftTextOffset.
984 MOZ_ASSERT(aContainer
[*aIndex
].mSoftTextOffset
> aSoftTextOffset
);
991 NodeOffset
mozInlineSpellWordUtil::MapSoftTextOffsetToDOMPosition(
992 int32_t aSoftTextOffset
, DOMMapHint aHint
) const {
993 NS_ASSERTION(mSoftTextValid
,
994 "Soft text must be valid if we're to map out of it");
995 if (!mSoftTextValid
) return NodeOffset(nullptr, -1);
997 // Find the last mapping, if any, such that mSoftTextOffset <= aSoftTextOffset
1000 FindLastNongreaterOffset(mSoftTextDOMMapping
, aSoftTextOffset
, &index
);
1002 return NodeOffset(nullptr, -1);
1005 // 'index' is now the last mapping, if any, such that
1006 // mSoftTextOffset <= aSoftTextOffset.
1007 // If we're doing HINT_END, then we may want to return the end of the
1008 // the previous mapping instead of the start of this mapping
1009 if (aHint
== HINT_END
&& index
> 0) {
1010 const DOMTextMapping
& map
= mSoftTextDOMMapping
[index
- 1];
1011 if (map
.mSoftTextOffset
+ map
.mLength
== aSoftTextOffset
)
1012 return NodeOffset(map
.mNodeOffset
.mNode
,
1013 map
.mNodeOffset
.mOffset
+ map
.mLength
);
1016 // We allow ourselves to return the end of this mapping even if we're
1017 // doing HINT_START. This will only happen if there is no mapping which this
1018 // point is the start of. I'm not 100% sure this is OK...
1019 const DOMTextMapping
& map
= mSoftTextDOMMapping
[index
];
1020 int32_t offset
= aSoftTextOffset
- map
.mSoftTextOffset
;
1021 if (offset
>= 0 && offset
<= map
.mLength
)
1022 return NodeOffset(map
.mNodeOffset
.mNode
, map
.mNodeOffset
.mOffset
+ offset
);
1024 return NodeOffset(nullptr, -1);
1028 void mozInlineSpellWordUtil::ToString(const DOMMapHint aHint
,
1029 nsACString
& aResult
) {
1032 aResult
.AssignLiteral("begin");
1035 aResult
.AssignLiteral("end");
1040 int32_t mozInlineSpellWordUtil::FindRealWordContaining(
1041 int32_t aSoftTextOffset
, DOMMapHint aHint
, bool aSearchForward
) const {
1042 if (MOZ_LOG_TEST(sInlineSpellWordUtilLog
, LogLevel::Debug
)) {
1044 mozInlineSpellWordUtil::ToString(aHint
, hint
);
1047 sInlineSpellWordUtilLog
, LogLevel::Debug
,
1048 ("%s: offset=%i, hint=%s, searchForward=%i.", __FUNCTION__
,
1049 aSoftTextOffset
, hint
.get(), static_cast<int32_t>(aSearchForward
)));
1052 NS_ASSERTION(mSoftTextValid
,
1053 "Soft text must be valid if we're to map out of it");
1054 if (!mSoftTextValid
) return -1;
1056 // Find the last word, if any, such that mSoftTextOffset <= aSoftTextOffset
1058 bool found
= FindLastNongreaterOffset(mRealWords
, aSoftTextOffset
, &index
);
1063 // 'index' is now the last word, if any, such that
1064 // mSoftTextOffset <= aSoftTextOffset.
1065 // If we're doing HINT_END, then we may want to return the end of the
1066 // the previous word instead of the start of this word
1067 if (aHint
== HINT_END
&& index
> 0) {
1068 const RealWord
& word
= mRealWords
[index
- 1];
1069 if (word
.mSoftTextOffset
+ word
.mLength
== aSoftTextOffset
)
1073 // We allow ourselves to return the end of this word even if we're
1074 // doing HINT_START. This will only happen if there is no word which this
1075 // point is the start of. I'm not 100% sure this is OK...
1076 const RealWord
& word
= mRealWords
[index
];
1077 int32_t offset
= aSoftTextOffset
- word
.mSoftTextOffset
;
1078 if (offset
>= 0 && offset
<= static_cast<int32_t>(word
.mLength
)) return index
;
1080 if (aSearchForward
) {
1081 if (mRealWords
[0].mSoftTextOffset
> aSoftTextOffset
) {
1082 // All words have mSoftTextOffset > aSoftTextOffset
1085 // 'index' is the last word such that mSoftTextOffset <= aSoftTextOffset.
1086 // Word index+1, if it exists, will be the first with
1087 // mSoftTextOffset > aSoftTextOffset.
1088 if (index
+ 1 < mRealWords
.Length()) return index
+ 1;
1094 // mozInlineSpellWordUtil::SplitDOMWordAndAppendTo
1096 nsresult
mozInlineSpellWordUtil::SplitDOMWordAndAppendTo(
1097 int32_t aStart
, int32_t aEnd
, nsTArray
<RealWord
>& aRealWords
) const {
1098 nsDependentSubstring
targetText(mSoftText
, aStart
, aEnd
- aStart
);
1099 WordSplitState
<nsDependentSubstring
> state(targetText
);
1100 state
.mCurCharClass
= state
.ClassifyCharacter(0, true);
1102 state
.AdvanceThroughSeparators();
1103 if (state
.mCurCharClass
!= CHAR_CLASS_END_OF_INPUT
&& state
.IsSpecialWord()) {
1104 int32_t specialWordLength
=
1105 state
.mDOMWordText
.Length() - state
.mDOMWordOffset
;
1106 if (!aRealWords
.AppendElement(
1107 RealWord(aStart
+ state
.mDOMWordOffset
, specialWordLength
, false),
1109 return NS_ERROR_OUT_OF_MEMORY
;
1115 while (state
.mCurCharClass
!= CHAR_CLASS_END_OF_INPUT
) {
1116 state
.AdvanceThroughSeparators();
1117 if (state
.mCurCharClass
== CHAR_CLASS_END_OF_INPUT
) break;
1119 // save the beginning of the word
1120 int32_t wordOffset
= state
.mDOMWordOffset
;
1122 // find the end of the word
1123 state
.AdvanceThroughWord();
1124 int32_t wordLen
= state
.mDOMWordOffset
- wordOffset
;
1125 if (!aRealWords
.AppendElement(
1126 RealWord(aStart
+ wordOffset
, wordLen
,
1127 !state
.ShouldSkipWord(wordOffset
, wordLen
)),
1129 return NS_ERROR_OUT_OF_MEMORY
;