mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-14 04:03:47 +00:00
Bug 626660, part2 - fix text diff algorithm, r=davidb, sr=neil, a=betaN
This commit is contained in:
parent
0678b1c95d
commit
e4cf999060
@ -77,6 +77,7 @@ CPPSRCS = \
|
|||||||
nsTextAccessible.cpp \
|
nsTextAccessible.cpp \
|
||||||
nsTextEquivUtils.cpp \
|
nsTextEquivUtils.cpp \
|
||||||
nsTextAttrs.cpp \
|
nsTextAttrs.cpp \
|
||||||
|
TextUpdater.cpp \
|
||||||
$(NULL)
|
$(NULL)
|
||||||
|
|
||||||
EXPORTS = \
|
EXPORTS = \
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
#include "nsDocAccessible.h"
|
#include "nsDocAccessible.h"
|
||||||
#include "nsEventShell.h"
|
#include "nsEventShell.h"
|
||||||
#include "nsTextAccessible.h"
|
#include "nsTextAccessible.h"
|
||||||
|
#include "TextUpdater.h"
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -574,287 +575,6 @@ NotificationController::CreateTextChangeEventFor(AccMutationEvent* aEvent)
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Notification controller: text leaf accessible text update
|
// Notification controller: text leaf accessible text update
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to find a difference between old and new text and fire text change
|
|
||||||
* events.
|
|
||||||
*/
|
|
||||||
class TextUpdater
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TextUpdater(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf) :
|
|
||||||
mDocument(aDocument), mTextLeaf(aTextLeaf) { }
|
|
||||||
~TextUpdater() { mDocument = nsnull; mTextLeaf = nsnull; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update text of the text leaf accessible, fire text change events for its
|
|
||||||
* container hypertext accessible.
|
|
||||||
*/
|
|
||||||
void Run(const nsAString& aNewText);
|
|
||||||
|
|
||||||
private:
|
|
||||||
TextUpdater();
|
|
||||||
TextUpdater(const TextUpdater&);
|
|
||||||
TextUpdater& operator = (const TextUpdater&);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fire text change events based on difference between strings.
|
|
||||||
*/
|
|
||||||
void FindDiffNFireEvents(const nsDependentSubstring& aStr1,
|
|
||||||
const nsDependentSubstring& aStr2,
|
|
||||||
PRUint32** aMatrix,
|
|
||||||
PRUint32 aStartOffset);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change type used to describe the diff between strings.
|
|
||||||
*/
|
|
||||||
enum ChangeType {
|
|
||||||
eNoChange,
|
|
||||||
eInsertion,
|
|
||||||
eRemoval,
|
|
||||||
eSubstitution
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to fire text change events.
|
|
||||||
*/
|
|
||||||
inline void MayFireEvent(nsAString* aInsertedText, nsAString* aRemovedText,
|
|
||||||
PRUint32 aOffset, ChangeType* aChange)
|
|
||||||
{
|
|
||||||
if (*aChange == eNoChange)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (*aChange == eRemoval || *aChange == eSubstitution) {
|
|
||||||
FireEvent(*aRemovedText, aOffset, PR_FALSE);
|
|
||||||
aRemovedText->Truncate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (*aChange == eInsertion || *aChange == eSubstitution) {
|
|
||||||
FireEvent(*aInsertedText, aOffset, PR_TRUE);
|
|
||||||
aInsertedText->Truncate();
|
|
||||||
}
|
|
||||||
|
|
||||||
*aChange = eNoChange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fire text change event.
|
|
||||||
*/
|
|
||||||
void FireEvent(const nsAString& aModText, PRUint32 aOffset, PRBool aType);
|
|
||||||
|
|
||||||
private:
|
|
||||||
nsDocAccessible* mDocument;
|
|
||||||
nsTextAccessible* mTextLeaf;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
TextUpdater::Run(const nsAString& aNewText)
|
|
||||||
{
|
|
||||||
NS_ASSERTION(mTextLeaf, "No text leaf accessible?");
|
|
||||||
|
|
||||||
const nsString& oldText = mTextLeaf->Text();
|
|
||||||
PRUint32 oldLen = oldText.Length(), newLen = aNewText.Length();
|
|
||||||
PRUint32 minLen = oldLen < newLen ? oldLen : newLen;
|
|
||||||
|
|
||||||
// Skip coinciding begin substrings.
|
|
||||||
PRUint32 skipIdx = 0;
|
|
||||||
for (; skipIdx < minLen; skipIdx++) {
|
|
||||||
if (aNewText[skipIdx] != oldText[skipIdx])
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No change, text append or removal to/from the end.
|
|
||||||
if (skipIdx == minLen) {
|
|
||||||
if (oldLen == newLen)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// If text has been appended to the end, fire text inserted event.
|
|
||||||
if (oldLen < newLen) {
|
|
||||||
FireEvent(Substring(aNewText, oldLen), oldLen, PR_TRUE);
|
|
||||||
mTextLeaf->SetText(aNewText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text has been removed from the end, fire text removed event.
|
|
||||||
FireEvent(Substring(oldText, newLen), newLen, PR_FALSE);
|
|
||||||
mTextLeaf->SetText(aNewText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim coinciding substrings from the end.
|
|
||||||
PRUint32 endIdx = minLen;
|
|
||||||
if (oldLen < newLen) {
|
|
||||||
PRUint32 delta = newLen - oldLen;
|
|
||||||
for (; endIdx > skipIdx; endIdx--) {
|
|
||||||
if (aNewText[endIdx + delta] != oldText[endIdx])
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PRUint32 delta = oldLen - newLen;
|
|
||||||
for (; endIdx > skipIdx; endIdx--) {
|
|
||||||
if (aNewText[endIdx] != oldText[endIdx + delta])
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PRUint32 oldEndIdx = oldLen - minLen + endIdx;
|
|
||||||
PRUint32 newEndIdx = newLen - minLen + endIdx;
|
|
||||||
|
|
||||||
// Find the difference starting from start character, we can skip initial and
|
|
||||||
// final coinciding characters since they don't affect on the Levenshtein
|
|
||||||
// distance.
|
|
||||||
|
|
||||||
const nsDependentSubstring& str1 =
|
|
||||||
Substring(oldText, skipIdx, oldEndIdx - skipIdx);
|
|
||||||
const nsDependentSubstring& str2 =
|
|
||||||
Substring(aNewText, skipIdx, newEndIdx - skipIdx);
|
|
||||||
|
|
||||||
// Compute the matrix.
|
|
||||||
PRUint32 len1 = str1.Length() + 1, len2 = str2.Length() + 1;
|
|
||||||
|
|
||||||
PRUint32** matrix = new PRUint32*[len1];
|
|
||||||
for (PRUint32 i = 0; i < len1; i++)
|
|
||||||
matrix[i] = new PRUint32[len2];
|
|
||||||
|
|
||||||
matrix[0][0] = 0;
|
|
||||||
|
|
||||||
for (PRUint32 i = 1; i < len1; i++)
|
|
||||||
matrix[i][0] = i;
|
|
||||||
|
|
||||||
for (PRUint32 j = 1; j < len2; j++)
|
|
||||||
matrix[0][j] = j;
|
|
||||||
|
|
||||||
for (PRUint32 i = 1; i < len1; i++) {
|
|
||||||
for (PRUint32 j = 1; j < len2; j++) {
|
|
||||||
if (str1[i - 1] != str2[j - 1]) {
|
|
||||||
PRUint32 left = matrix[i - 1][j];
|
|
||||||
PRUint32 up = matrix[i][j - 1];
|
|
||||||
|
|
||||||
PRUint32 upleft = matrix[i - 1][j - 1];
|
|
||||||
matrix[i][j] =
|
|
||||||
(left < up ? (upleft < left ? upleft : left) :
|
|
||||||
(upleft < up ? upleft : up)) + 1;
|
|
||||||
} else {
|
|
||||||
matrix[i][j] = matrix[i - 1][j - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FindDiffNFireEvents(str1, str2, matrix, skipIdx);
|
|
||||||
|
|
||||||
for (PRUint32 i = 0; i < len1; i++)
|
|
||||||
delete[] matrix[i];
|
|
||||||
delete[] matrix;
|
|
||||||
|
|
||||||
mTextLeaf->SetText(aNewText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TextUpdater::FindDiffNFireEvents(const nsDependentSubstring& aStr1,
|
|
||||||
const nsDependentSubstring& aStr2,
|
|
||||||
PRUint32** aMatrix,
|
|
||||||
PRUint32 aStartOffset)
|
|
||||||
{
|
|
||||||
// Find the difference.
|
|
||||||
ChangeType change = eNoChange;
|
|
||||||
nsAutoString insertedText;
|
|
||||||
nsAutoString removedText;
|
|
||||||
PRUint32 offset = 0;
|
|
||||||
|
|
||||||
PRInt32 i = aStr1.Length(), j = aStr2.Length();
|
|
||||||
while (i >= 0 && j >= 0) {
|
|
||||||
if (aMatrix[i][j] == 0) {
|
|
||||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// move up left
|
|
||||||
if (i >= 1 && j >= 1) {
|
|
||||||
// no change
|
|
||||||
if (aStr1[i - 1] == aStr2[j - 1]) {
|
|
||||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
|
||||||
|
|
||||||
i--; j--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// substitution
|
|
||||||
if (aMatrix[i][j] == aMatrix[i - 1][j - 1] + 1) {
|
|
||||||
if (change != eSubstitution)
|
|
||||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
|
||||||
|
|
||||||
offset = j - 1;
|
|
||||||
insertedText.Append(aStr1[i - 1]);
|
|
||||||
removedText.Append(aStr2[offset]);
|
|
||||||
change = eSubstitution;
|
|
||||||
|
|
||||||
i--; j--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// move up, insertion
|
|
||||||
if (j >= 1 && aMatrix[i][j] == aMatrix[i][j - 1] + 1) {
|
|
||||||
if (change != eInsertion)
|
|
||||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
|
||||||
|
|
||||||
offset = j - 1;
|
|
||||||
insertedText.Insert(aStr2[offset], 0);
|
|
||||||
change = eInsertion;
|
|
||||||
|
|
||||||
j--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// move left, removal
|
|
||||||
if (i >= 1 && aMatrix[i][j] == aMatrix[i - 1][j] + 1) {
|
|
||||||
if (change != eRemoval) {
|
|
||||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
|
||||||
|
|
||||||
offset = j;
|
|
||||||
}
|
|
||||||
|
|
||||||
removedText.Insert(aStr1[i - 1], 0);
|
|
||||||
change = eRemoval;
|
|
||||||
|
|
||||||
i--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
NS_NOTREACHED("Huh?");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MayFireEvent(&insertedText, &removedText, offset + aStartOffset, &change);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
TextUpdater::FireEvent(const nsAString& aModText, PRUint32 aOffset,
|
|
||||||
PRBool aIsInserted)
|
|
||||||
{
|
|
||||||
nsAccessible* parent = mTextLeaf->GetParent();
|
|
||||||
NS_ASSERTION(parent, "No parent for text leaf!");
|
|
||||||
|
|
||||||
nsHyperTextAccessible* hyperText = parent->AsHyperText();
|
|
||||||
NS_ASSERTION(hyperText, "Text leaf parnet is not hyper text!");
|
|
||||||
|
|
||||||
PRInt32 textLeafOffset = hyperText->GetChildOffset(mTextLeaf, PR_TRUE);
|
|
||||||
NS_ASSERTION(textLeafOffset != -1,
|
|
||||||
"Text leaf hasn't offset within hyper text!");
|
|
||||||
|
|
||||||
// Fire text change event.
|
|
||||||
nsRefPtr<AccEvent> textChangeEvent =
|
|
||||||
new AccTextChangeEvent(hyperText, textLeafOffset + aOffset, aModText,
|
|
||||||
aIsInserted);
|
|
||||||
mDocument->FireDelayedAccessibleEvent(textChangeEvent);
|
|
||||||
|
|
||||||
// Fire value change event.
|
|
||||||
if (hyperText->Role() == nsIAccessibleRole::ROLE_ENTRY) {
|
|
||||||
nsRefPtr<AccEvent> valueChangeEvent =
|
|
||||||
new AccEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, hyperText,
|
|
||||||
eAutoDetect, AccEvent::eRemoveDupes);
|
|
||||||
mDocument->FireDelayedAccessibleEvent(valueChangeEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PLDHashOperator
|
PLDHashOperator
|
||||||
NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
|
NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
|
||||||
void* aUserArg)
|
void* aUserArg)
|
||||||
@ -927,9 +647,7 @@ NotificationController::TextEnumerator(nsCOMPtrHashKey<nsIContent>* aEntry,
|
|||||||
NS_ConvertUTF16toUTF8(text).get());
|
NS_ConvertUTF16toUTF8(text).get());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
TextUpdater updater(document, textAcc->AsTextLeaf());
|
TextUpdater::Run(document, textAcc->AsTextLeaf(), text);
|
||||||
updater.Run(text);
|
|
||||||
|
|
||||||
return PL_DHASH_NEXT;
|
return PL_DHASH_NEXT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
263
accessible/src/base/TextUpdater.cpp
Normal file
263
accessible/src/base/TextUpdater.cpp
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is mozilla.org code.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is
|
||||||
|
* Mozilla Foundation.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Alexander Surkov <surkov.alexander@gmail.com> (original author)
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
||||||
|
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
#include "TextUpdater.h"
|
||||||
|
|
||||||
|
#include "nsDocAccessible.h"
|
||||||
|
#include "nsTextAccessible.h"
|
||||||
|
|
||||||
|
void
|
||||||
|
TextUpdater::Run(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf,
|
||||||
|
const nsAString& aNewText)
|
||||||
|
{
|
||||||
|
NS_ASSERTION(aTextLeaf, "No text leaf accessible?");
|
||||||
|
|
||||||
|
const nsString& oldText = aTextLeaf->Text();
|
||||||
|
PRUint32 oldLen = oldText.Length(), newLen = aNewText.Length();
|
||||||
|
PRUint32 minLen = NS_MIN(oldLen, newLen);
|
||||||
|
|
||||||
|
// Skip coinciding begin substrings.
|
||||||
|
PRUint32 skipStart = 0;
|
||||||
|
for (; skipStart < minLen; skipStart++) {
|
||||||
|
if (aNewText[skipStart] != oldText[skipStart])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The text was changed. Do update.
|
||||||
|
if (skipStart != minLen || oldLen != newLen) {
|
||||||
|
TextUpdater updater(aDocument, aTextLeaf);
|
||||||
|
updater.DoUpdate(aNewText, oldText, skipStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TextUpdater::DoUpdate(const nsAString& aNewText, const nsAString& aOldText,
|
||||||
|
PRUint32 aSkipStart)
|
||||||
|
{
|
||||||
|
nsAccessible* parent = mTextLeaf->GetParent();
|
||||||
|
NS_ASSERTION(parent, "No parent for text leaf!");
|
||||||
|
|
||||||
|
mHyperText = parent->AsHyperText();
|
||||||
|
if (!mHyperText) {
|
||||||
|
NS_ERROR("Text leaf parent is not hypertext!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the text leaf accessible offset and invalidate cached offsets after it.
|
||||||
|
mTextOffset = mHyperText->GetChildOffset(mTextLeaf, PR_TRUE);
|
||||||
|
NS_ASSERTION(mTextOffset != -1,
|
||||||
|
"Text leaf hasn't offset within hyper text!");
|
||||||
|
|
||||||
|
PRUint32 oldLen = aOldText.Length(), newLen = aNewText.Length();
|
||||||
|
PRUint32 minLen = NS_MIN(oldLen, newLen);
|
||||||
|
|
||||||
|
// Text was appended or removed to/from the end.
|
||||||
|
if (aSkipStart == minLen) {
|
||||||
|
// If text has been appended to the end, fire text inserted event.
|
||||||
|
if (oldLen < newLen) {
|
||||||
|
UpdateTextNFireEvent(aNewText, Substring(aNewText, oldLen),
|
||||||
|
oldLen, PR_TRUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text has been removed from the end, fire text removed event.
|
||||||
|
UpdateTextNFireEvent(aNewText, Substring(aOldText, newLen),
|
||||||
|
newLen, PR_FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim coinciding substrings from the end.
|
||||||
|
PRUint32 skipEnd = 0;
|
||||||
|
while (minLen - skipEnd > aSkipStart &&
|
||||||
|
aNewText[newLen - skipEnd - 1] == aOldText[oldLen - skipEnd - 1]) {
|
||||||
|
skipEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text was appended or removed to/from the start.
|
||||||
|
if (skipEnd == minLen) {
|
||||||
|
// If text has been appended to the start, fire text inserted event.
|
||||||
|
if (oldLen < newLen) {
|
||||||
|
UpdateTextNFireEvent(aNewText, Substring(aNewText, 0, newLen - skipEnd),
|
||||||
|
0, PR_TRUE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text has been removed from the start, fire text removed event.
|
||||||
|
UpdateTextNFireEvent(aNewText, Substring(aOldText, 0, oldLen - skipEnd),
|
||||||
|
0, PR_FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the difference between strings and fire events.
|
||||||
|
// Note: we can skip initial and final coinciding characters since they don't
|
||||||
|
// affect the Levenshtein distance.
|
||||||
|
|
||||||
|
PRInt32 strLen1 = oldLen - aSkipStart - skipEnd;
|
||||||
|
PRInt32 strLen2 = newLen - aSkipStart - skipEnd;
|
||||||
|
|
||||||
|
const nsAString& str1 = Substring(aOldText, aSkipStart, strLen1);
|
||||||
|
const nsAString& str2 = Substring(aNewText, aSkipStart, strLen2);
|
||||||
|
|
||||||
|
// Increase offset of the text leaf on skipped characters amount.
|
||||||
|
mTextOffset += aSkipStart;
|
||||||
|
|
||||||
|
// Compute the flat structured matrix need to compute the difference.
|
||||||
|
PRUint32 len1 = strLen1 + 1, len2 = strLen2 + 1;
|
||||||
|
PRUint32* entries = new PRUint32[len1 * len2];
|
||||||
|
|
||||||
|
for (PRUint32 colIdx = 0; colIdx < len1; colIdx++)
|
||||||
|
entries[colIdx] = colIdx;
|
||||||
|
|
||||||
|
PRUint32* row = entries;
|
||||||
|
for (PRUint32 rowIdx = 1; rowIdx < len2; rowIdx++) {
|
||||||
|
PRUint32* prevRow = row;
|
||||||
|
row += len1;
|
||||||
|
row[0] = rowIdx;
|
||||||
|
for (PRUint32 colIdx = 1; colIdx < len1; colIdx++) {
|
||||||
|
if (str1[colIdx - 1] != str2[rowIdx - 1]) {
|
||||||
|
PRUint32 left = row[colIdx - 1];
|
||||||
|
PRUint32 up = prevRow[colIdx];
|
||||||
|
PRUint32 upleft = prevRow[colIdx - 1];
|
||||||
|
row[colIdx] = NS_MIN(upleft, NS_MIN(left, up)) + 1;
|
||||||
|
} else {
|
||||||
|
row[colIdx] = prevRow[colIdx - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute events based on the difference.
|
||||||
|
nsTArray<nsRefPtr<AccEvent> > events;
|
||||||
|
ComputeTextChangeEvents(str1, str2, entries, events);
|
||||||
|
|
||||||
|
delete [] entries;
|
||||||
|
|
||||||
|
// Fire events.
|
||||||
|
for (PRInt32 idx = events.Length() - 1; idx >= 0; idx--)
|
||||||
|
mDocument->FireDelayedAccessibleEvent(events[idx]);
|
||||||
|
|
||||||
|
if (mHyperText->Role() == nsIAccessibleRole::ROLE_ENTRY) {
|
||||||
|
nsRefPtr<AccEvent> valueChangeEvent =
|
||||||
|
new AccEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, mHyperText,
|
||||||
|
eAutoDetect, AccEvent::eRemoveDupes);
|
||||||
|
mDocument->FireDelayedAccessibleEvent(valueChangeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the text.
|
||||||
|
mTextLeaf->SetText(aNewText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TextUpdater::ComputeTextChangeEvents(const nsAString& aStr1,
|
||||||
|
const nsAString& aStr2,
|
||||||
|
PRUint32* aEntries,
|
||||||
|
nsTArray<nsRefPtr<AccEvent> >& aEvents)
|
||||||
|
{
|
||||||
|
PRInt32 colIdx = aStr1.Length(), rowIdx = aStr2.Length();
|
||||||
|
|
||||||
|
// Point at which strings last matched.
|
||||||
|
PRInt32 colEnd = colIdx;
|
||||||
|
PRInt32 rowEnd = rowIdx;
|
||||||
|
|
||||||
|
PRInt32 colLen = colEnd + 1;
|
||||||
|
PRUint32* row = aEntries + rowIdx * colLen;
|
||||||
|
PRInt32 dist = row[colIdx]; // current Levenshtein distance
|
||||||
|
while (rowIdx && colIdx) { // stop when we can't move diagonally
|
||||||
|
if (aStr1[colIdx - 1] == aStr2[rowIdx - 1]) { // match
|
||||||
|
if (rowIdx < rowEnd) { // deal with any pending insertion
|
||||||
|
FireInsertEvent(Substring(aStr2, rowIdx, rowEnd - rowIdx),
|
||||||
|
rowIdx, aEvents);
|
||||||
|
}
|
||||||
|
if (colIdx < colEnd) { // deal with any pending deletion
|
||||||
|
FireDeleteEvent(Substring(aStr1, colIdx, colEnd - colIdx),
|
||||||
|
rowIdx, aEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
colEnd = --colIdx; // reset the match point
|
||||||
|
rowEnd = --rowIdx;
|
||||||
|
row -= colLen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
--dist;
|
||||||
|
if (dist == row[colIdx - 1 - colLen]) { // substitution
|
||||||
|
--colIdx;
|
||||||
|
--rowIdx;
|
||||||
|
row -= colLen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dist == row[colIdx - colLen]) { // insertion
|
||||||
|
--rowIdx;
|
||||||
|
row -= colLen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (dist == row[colIdx - 1]) { // deletion
|
||||||
|
--colIdx;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
NS_NOTREACHED("huh?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowEnd)
|
||||||
|
FireInsertEvent(Substring(aStr2, 0, rowEnd), 0, aEvents);
|
||||||
|
if (colEnd)
|
||||||
|
FireDeleteEvent(Substring(aStr1, 0, colEnd), 0, aEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
TextUpdater::UpdateTextNFireEvent(const nsAString& aNewText,
|
||||||
|
const nsAString& aChangeText,
|
||||||
|
PRUint32 aAddlOffset,
|
||||||
|
PRBool aIsInserted)
|
||||||
|
{
|
||||||
|
// Fire text change event.
|
||||||
|
nsRefPtr<AccEvent> textChangeEvent =
|
||||||
|
new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset, aChangeText,
|
||||||
|
aIsInserted);
|
||||||
|
mDocument->FireDelayedAccessibleEvent(textChangeEvent);
|
||||||
|
|
||||||
|
// Fire value change event.
|
||||||
|
if (mHyperText->Role() == nsIAccessibleRole::ROLE_ENTRY) {
|
||||||
|
nsRefPtr<AccEvent> valueChangeEvent =
|
||||||
|
new AccEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, mHyperText,
|
||||||
|
eAutoDetect, AccEvent::eRemoveDupes);
|
||||||
|
mDocument->FireDelayedAccessibleEvent(valueChangeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the text.
|
||||||
|
mTextLeaf->SetText(aNewText);
|
||||||
|
}
|
124
accessible/src/base/TextUpdater.h
Normal file
124
accessible/src/base/TextUpdater.h
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||||
|
/* ***** BEGIN LICENSE BLOCK *****
|
||||||
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||||||
|
*
|
||||||
|
* The contents of this file are subject to the Mozilla Public License Version
|
||||||
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
* http://www.mozilla.org/MPL/
|
||||||
|
*
|
||||||
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||||||
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||||||
|
* for the specific language governing rights and limitations under the
|
||||||
|
* License.
|
||||||
|
*
|
||||||
|
* The Original Code is mozilla.org code.
|
||||||
|
*
|
||||||
|
* The Initial Developer of the Original Code is
|
||||||
|
* Mozilla Foundation.
|
||||||
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
||||||
|
* the Initial Developer. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Contributor(s):
|
||||||
|
* Alexander Surkov <surkov.alexander@gmail.com> (original author)
|
||||||
|
*
|
||||||
|
* Alternatively, the contents of this file may be used under the terms of
|
||||||
|
* either of the GNU General Public License Version 2 or later (the "GPL"),
|
||||||
|
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||||||
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||||||
|
* of those above. If you wish to allow use of your version of this file only
|
||||||
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||||||
|
* use your version of this file under the terms of the MPL, indicate your
|
||||||
|
* decision by deleting the provisions above and replace them with the notice
|
||||||
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||||||
|
* the provisions above, a recipient may use your version of this file under
|
||||||
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||||||
|
*
|
||||||
|
* ***** END LICENSE BLOCK ***** */
|
||||||
|
|
||||||
|
#ifndef TextUpdater_h_
|
||||||
|
#define TextUpdater_h_
|
||||||
|
|
||||||
|
#include "AccEvent.h"
|
||||||
|
#include "nsHyperTextAccessible.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to find a difference between old and new text and fire text change
|
||||||
|
* events.
|
||||||
|
*/
|
||||||
|
class TextUpdater
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Start text of the text leaf update.
|
||||||
|
*/
|
||||||
|
static void Run(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf,
|
||||||
|
const nsAString& aNewText);
|
||||||
|
|
||||||
|
private:
|
||||||
|
TextUpdater(nsDocAccessible* aDocument, nsTextAccessible* aTextLeaf) :
|
||||||
|
mDocument(aDocument), mTextLeaf(aTextLeaf), mHyperText(nsnull),
|
||||||
|
mTextOffset(-1) { }
|
||||||
|
|
||||||
|
~TextUpdater()
|
||||||
|
{ mDocument = nsnull; mTextLeaf = nsnull; mHyperText = nsnull; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update text of the text leaf accessible, fire text change and value change
|
||||||
|
* (if applicable) events for its container hypertext accessible.
|
||||||
|
*/
|
||||||
|
void DoUpdate(const nsAString& aNewText, const nsAString& aOldText,
|
||||||
|
PRUint32 aSkipStart);
|
||||||
|
|
||||||
|
private:
|
||||||
|
TextUpdater();
|
||||||
|
TextUpdater(const TextUpdater&);
|
||||||
|
TextUpdater& operator=(const TextUpdater&);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire text change events based on difference between strings.
|
||||||
|
*/
|
||||||
|
void ComputeTextChangeEvents(const nsAString& aStr1,
|
||||||
|
const nsAString& aStr2,
|
||||||
|
PRUint32* aEntries,
|
||||||
|
nsTArray<nsRefPtr<AccEvent> >& aEvents);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create text change events for inserted text.
|
||||||
|
*/
|
||||||
|
inline void FireInsertEvent(const nsAString& aText, PRUint32 aAddlOffset,
|
||||||
|
nsTArray<nsRefPtr<AccEvent> >& aEvents)
|
||||||
|
{
|
||||||
|
nsRefPtr<AccEvent> event =
|
||||||
|
new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset,
|
||||||
|
aText, PR_TRUE);
|
||||||
|
aEvents.AppendElement(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to create text change events for removed text.
|
||||||
|
*/
|
||||||
|
inline void FireDeleteEvent(const nsAString& aText, PRUint32 aAddlOffset,
|
||||||
|
nsTArray<nsRefPtr<AccEvent> >& aEvents)
|
||||||
|
{
|
||||||
|
nsRefPtr<AccEvent> event =
|
||||||
|
new AccTextChangeEvent(mHyperText, mTextOffset + aAddlOffset,
|
||||||
|
aText, PR_FALSE);
|
||||||
|
aEvents.AppendElement(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the text and fire text change/value change events.
|
||||||
|
*/
|
||||||
|
void UpdateTextNFireEvent(const nsAString& aNewText,
|
||||||
|
const nsAString& aChangeText, PRUint32 aAddlOffset,
|
||||||
|
PRBool aIsInserted);
|
||||||
|
|
||||||
|
private:
|
||||||
|
nsDocAccessible* mDocument;
|
||||||
|
nsTextAccessible* mTextLeaf;
|
||||||
|
nsHyperTextAccessible* mHyperText;
|
||||||
|
PRInt32 mTextOffset;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
@ -331,7 +331,7 @@ function eventQueue(aEventType)
|
|||||||
var idx = 0;
|
var idx = 0;
|
||||||
for (; idx < this.mEventSeq.length; idx++) {
|
for (; idx < this.mEventSeq.length; idx++) {
|
||||||
if (!this.isEventUnexpected(idx) && (invoker.wasCaught[idx] == true) &&
|
if (!this.isEventUnexpected(idx) && (invoker.wasCaught[idx] == true) &&
|
||||||
this.compareEvents(idx, aEvent)) {
|
this.isAlreadyCaught(idx, aEvent)) {
|
||||||
|
|
||||||
var msg = "Doubled event { event type: " +
|
var msg = "Doubled event { event type: " +
|
||||||
this.getEventTypeAsString(idx) + ", target: " +
|
this.getEventTypeAsString(idx) + ", target: " +
|
||||||
@ -543,6 +543,15 @@ function eventQueue(aEventType)
|
|||||||
return target1 == target2;
|
return target1 == target2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isAlreadyCaught = function eventQueue_isAlreadyCaught(aIdx, aEvent)
|
||||||
|
{
|
||||||
|
// We don't have stored info about handled event other than its type and
|
||||||
|
// target, thus we should filter text change events since they may occur
|
||||||
|
// on the same element because of complex changes.
|
||||||
|
return this.compareEvents(aIdx, aEvent) &&
|
||||||
|
!(aEvent instanceof nsIAccessibleTextChangeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
this.checkEvent = function eventQueue_checkEvent(aIdx, aEvent)
|
this.checkEvent = function eventQueue_checkEvent(aIdx, aEvent)
|
||||||
{
|
{
|
||||||
var eventItem = this.mEventSeq[aIdx];
|
var eventItem = this.mEventSeq[aIdx];
|
||||||
|
@ -71,6 +71,7 @@ _TEST_FILES =\
|
|||||||
test_scroll.xul \
|
test_scroll.xul \
|
||||||
test_selection.html \
|
test_selection.html \
|
||||||
test_statechange.html \
|
test_statechange.html \
|
||||||
|
test_text_alg.html \
|
||||||
test_text.html \
|
test_text.html \
|
||||||
test_textattrchange.html \
|
test_textattrchange.html \
|
||||||
test_tree.xul \
|
test_tree.xul \
|
||||||
|
205
accessible/tests/mochitest/events/test_text_alg.html
Normal file
205
accessible/tests/mochitest/events/test_text_alg.html
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Accessible text update algorithm testing</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css"
|
||||||
|
href="chrome://mochikit/content/tests/SimpleTest/test.css" />
|
||||||
|
|
||||||
|
<script type="application/javascript"
|
||||||
|
src="chrome://mochikit/content/MochiKit/packed.js"></script>
|
||||||
|
<script type="application/javascript"
|
||||||
|
src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||||
|
<script type="application/javascript"
|
||||||
|
src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
|
||||||
|
|
||||||
|
<script type="application/javascript"
|
||||||
|
src="../common.js"></script>
|
||||||
|
<script type="application/javascript"
|
||||||
|
src="../events.js"></script>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Invokers
|
||||||
|
|
||||||
|
const kRemoval = 0;
|
||||||
|
const kInsertion = 1;
|
||||||
|
|
||||||
|
function changeText(aContainerID, aValue, aEventList)
|
||||||
|
{
|
||||||
|
this.containerNode = getNode(aContainerID);
|
||||||
|
this.textNode = this.containerNode.firstChild;
|
||||||
|
this.textData = this.textNode.data;
|
||||||
|
|
||||||
|
this.eventSeq = [ ];
|
||||||
|
for (var i = 0; i < aEventList.length; i++) {
|
||||||
|
var isInserted = aEventList[i][0];
|
||||||
|
var str = aEventList[i][1];
|
||||||
|
var offset = aEventList[i][2];
|
||||||
|
var checker = new textChangeChecker(this.containerNode, offset,
|
||||||
|
offset + str.length, str,
|
||||||
|
isInserted);
|
||||||
|
this.eventSeq.push(checker);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.invoke = function changeText_invoke()
|
||||||
|
{
|
||||||
|
this.textNode.data = aValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getID = function changeText_getID()
|
||||||
|
{
|
||||||
|
return "change text '" + this.textData + "' -> " + this.textNode.data +
|
||||||
|
"for " + prettyName(this.containerNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Do tests
|
||||||
|
|
||||||
|
//gA11yEventDumpID = "eventdump"; // debug stuff
|
||||||
|
//gA11yEventDumpToConsole = true;
|
||||||
|
|
||||||
|
var gQueue = null;
|
||||||
|
function doTests()
|
||||||
|
{
|
||||||
|
gQueue = new eventQueue();
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// wqrema -> tqb: substitution coalesced with removal
|
||||||
|
|
||||||
|
var events = [
|
||||||
|
[ kRemoval, "w", 0 ], // wqrema -> qrema
|
||||||
|
[ kInsertion, "t", 0], // qrema -> tqrema
|
||||||
|
[ kRemoval, "rema", 2 ], // tqrema -> tq
|
||||||
|
[ kInsertion, "b", 2] // tq -> tqb
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p1", "tqb", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// b -> insa: substitution coalesced with insertion (complex substitution)
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "b", 0 ], // b ->
|
||||||
|
[ kInsertion, "insa", 0] // -> insa
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p2", "insa", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// abc -> def: coalesced substitutions
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "abc", 0 ], // abc ->
|
||||||
|
[ kInsertion, "def", 0] // -> def
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p3", "def", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// abcabc -> abcDEFabc: coalesced insertions
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kInsertion, "DEF", 3] // abcabc -> abcDEFabc
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p4", "abcDEFabc", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// abc -> defabc: insertion into begin
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kInsertion, "def", 0] // abc -> defabc
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p5", "defabc", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// abc -> abcdef: insertion into end
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kInsertion, "def", 3] // abc -> abcdef
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p6", "abcdef", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// defabc -> abc: removal from begin
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "def", 0] // defabc -> abc
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p7", "abc", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// abcdef -> abc: removal from the end
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "def", 3] // abcdef -> abc
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p8", "abc", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// abcDEFabc -> abcabc: coalesced removals
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "DEF", 3] // abcDEFabc -> abcabc
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p9", "abcabc", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// !abcdef@ -> @axbcef!: insertion, deletion and substitutions
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "!", 0 ], // !abcdef@ -> abcdef@
|
||||||
|
[ kInsertion, "@", 0], // abcdef@ -> @abcdef@
|
||||||
|
[ kInsertion, "x", 2 ], // @abcdef@ -> @axbcdef@
|
||||||
|
[ kRemoval, "d", 5], // @axbcdef@ -> @axbcef@
|
||||||
|
[ kRemoval, "@", 7 ], // @axbcef@ -> @axbcef
|
||||||
|
[ kInsertion, "!", 7 ], // @axbcef -> @axbcef!
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p10", "@axbcef!", events));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
// meilenstein -> levenshtein: insertion, complex and simple substitutions
|
||||||
|
|
||||||
|
events = [
|
||||||
|
[ kRemoval, "m", 0 ], // meilenstein -> eilenstein
|
||||||
|
[ kInsertion, "l", 0], // eilenstein -> leilenstein
|
||||||
|
[ kRemoval, "il", 2 ], // leilenstein -> leenstein
|
||||||
|
[ kInsertion, "v", 2], // leenstein -> levenstein
|
||||||
|
[ kInsertion, "h", 6 ], // levenstein -> levenshtein
|
||||||
|
];
|
||||||
|
gQueue.push(new changeText("p11", "levenshtein", events));
|
||||||
|
|
||||||
|
gQueue.invoke(); // Will call SimpleTest.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleTest.waitForExplicitFinish();
|
||||||
|
addA11yLoadEvent(doTests);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<a target="_blank"
|
||||||
|
href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"
|
||||||
|
title="Cache rendered text on a11y side">
|
||||||
|
Mozilla Bug 626660
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p id="display"></p>
|
||||||
|
<div id="content" style="display: none"></div>
|
||||||
|
<pre id="test">
|
||||||
|
</pre>
|
||||||
|
<div id="eventdump"></div>
|
||||||
|
|
||||||
|
<p id="p1">wqrema</p>
|
||||||
|
<p id="p2">b</p>
|
||||||
|
<p id="p3">abc</p>
|
||||||
|
<p id="p4">abcabc</p>
|
||||||
|
<p id="p5">abc</p>
|
||||||
|
<p id="p6">abc</p>
|
||||||
|
<p id="p7">defabc</p>
|
||||||
|
<p id="p8">abcdef</p>
|
||||||
|
<p id="p9">abcDEFabc</p>
|
||||||
|
<p id="p10">!abcdef@</p>
|
||||||
|
<p id="p11">meilenstein</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user