ChangeStyleTransaction.cpp (13474B)
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/. */ 5 6 #include "ChangeStyleTransaction.h" 7 8 #include "EditorDOMAPIWrapper.h" 9 #include "HTMLEditor.h" 10 #include "HTMLEditUtils.h" 11 #include "mozilla/Logging.h" 12 #include "mozilla/ToString.h" 13 #include "mozilla/dom/Element.h" // for Element 14 #include "nsAString.h" // for nsAString::Append, etc. 15 #include "nsCRT.h" // for nsCRT::IsAsciiSpace 16 #include "nsDebug.h" // for NS_WARNING, etc. 17 #include "nsError.h" // for NS_ERROR_NULL_POINTER, etc. 18 #include "nsGkAtoms.h" // for nsGkAtoms, etc. 19 #include "nsICSSDeclaration.h" // for nsICSSDeclaration. 20 #include "nsLiteralString.h" // for NS_LITERAL_STRING, etc. 21 #include "nsReadableUtils.h" // for ToNewUnicode 22 #include "nsString.h" // for nsAutoString, nsString, etc. 23 #include "nsStyledElement.h" // for nsStyledElement. 24 #include "nsUnicharUtils.h" // for nsCaseInsensitiveStringComparator 25 26 namespace mozilla { 27 28 using namespace dom; 29 30 // static 31 already_AddRefed<ChangeStyleTransaction> ChangeStyleTransaction::Create( 32 HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement, nsAtom& aProperty, 33 const nsAString& aValue) { 34 RefPtr<ChangeStyleTransaction> transaction = new ChangeStyleTransaction( 35 aHTMLEditor, aStyledElement, aProperty, aValue, false); 36 return transaction.forget(); 37 } 38 39 // static 40 already_AddRefed<ChangeStyleTransaction> ChangeStyleTransaction::CreateToRemove( 41 HTMLEditor& aHTMLEditor, nsStyledElement& aStyledElement, nsAtom& aProperty, 42 const nsAString& aValue) { 43 RefPtr<ChangeStyleTransaction> transaction = new ChangeStyleTransaction( 44 aHTMLEditor, aStyledElement, aProperty, aValue, true); 45 return transaction.forget(); 46 } 47 48 ChangeStyleTransaction::ChangeStyleTransaction(HTMLEditor& aHTMLEditor, 49 nsStyledElement& aStyledElement, 50 nsAtom& aProperty, 51 const nsAString& aValue, 52 bool aRemove) 53 : EditTransactionBase(), 54 mHTMLEditor(&aHTMLEditor), 55 mStyledElement(&aStyledElement), 56 mProperty(&aProperty), 57 mRemoveProperty(aRemove), 58 mUndoAttributeWasSet(false), 59 mRedoAttributeWasSet(false) { 60 CopyUTF16toUTF8(aValue, mValue); 61 } 62 63 std::ostream& operator<<(std::ostream& aStream, 64 const ChangeStyleTransaction& aTransaction) { 65 aStream << "{ mStyledElement=" << aTransaction.mStyledElement.get(); 66 if (aTransaction.mStyledElement) { 67 aStream << " (" << *aTransaction.mStyledElement << ")"; 68 } 69 aStream << ", mProperty=" << nsAtomCString(aTransaction.mProperty).get() 70 << ", mValue=\"" << aTransaction.mValue.get() << "\", mUndoValue=\"" 71 << aTransaction.mUndoValue.get() 72 << "\", mRedoValue=" << aTransaction.mRedoValue.get() 73 << ", mRemoveProperty=" 74 << (aTransaction.mRemoveProperty ? "true" : "false") 75 << ", mUndoAttributeWasSet=" 76 << (aTransaction.mUndoAttributeWasSet ? "true" : "false") 77 << ", mRedoAttributeWasSet=" 78 << (aTransaction.mRedoAttributeWasSet ? "true" : "false") 79 << ", mHTMLEditor=" << aTransaction.mHTMLEditor.get() << " }"; 80 return aStream; 81 } 82 83 #define kNullCh ('\0') 84 85 NS_IMPL_CYCLE_COLLECTION_INHERITED(ChangeStyleTransaction, EditTransactionBase, 86 mHTMLEditor, mStyledElement) 87 88 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ChangeStyleTransaction) 89 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) 90 91 NS_IMPL_ADDREF_INHERITED(ChangeStyleTransaction, EditTransactionBase) 92 NS_IMPL_RELEASE_INHERITED(ChangeStyleTransaction, EditTransactionBase) 93 94 // Answers true if aValue is in the string list of white-space separated values 95 // aValueList. 96 bool ChangeStyleTransaction::ValueIncludes(const nsACString& aValueList, 97 const nsACString& aValue) { 98 nsAutoCString valueList(aValueList); 99 bool result = false; 100 101 // put an extra null at the end 102 valueList.Append(kNullCh); 103 104 char* start = valueList.BeginWriting(); 105 char* end = start; 106 107 while (kNullCh != *start) { 108 while (kNullCh != *start && nsCRT::IsAsciiSpace(*start)) { 109 // skip leading space 110 start++; 111 } 112 end = start; 113 114 while (kNullCh != *end && !nsCRT::IsAsciiSpace(*end)) { 115 // look for space or end 116 end++; 117 } 118 // end string here 119 *end = kNullCh; 120 121 if (start < end) { 122 if (aValue.Equals(nsDependentCString(start), 123 nsCaseInsensitiveCStringComparator)) { 124 result = true; 125 break; 126 } 127 } 128 start = ++end; 129 } 130 return result; 131 } 132 133 NS_IMETHODIMP ChangeStyleTransaction::DoTransaction() { 134 MOZ_LOG(GetLogModule(), LogLevel::Info, 135 ("%p ChangeStyleTransaction::%s this=%s", this, __FUNCTION__, 136 ToString(*this).c_str())); 137 138 MOZ_ASSERT(mHTMLEditor); 139 if (NS_WARN_IF(!mStyledElement)) { 140 return NS_ERROR_NOT_AVAILABLE; 141 } 142 143 const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor; 144 const OwningNonNull<nsStyledElement> styledElement = *mStyledElement; 145 const nsCOMPtr<nsDOMCSSDeclaration> cssDecl = styledElement->Style(); 146 147 // FIXME(bug 1606994): Using atoms forces a string copy here which is not 148 // great. 149 nsAutoCString propertyNameString; 150 mProperty->ToUTF8String(propertyNameString); 151 152 mUndoAttributeWasSet = mStyledElement->HasAttr(nsGkAtoms::style); 153 154 nsAutoCString values; 155 cssDecl->GetPropertyValue(propertyNameString, values); 156 mUndoValue.Assign(values); 157 158 if (mRemoveProperty) { 159 if (mProperty == nsGkAtoms::text_decoration) { 160 BuildTextDecorationValueToRemove(values, mValue, values); 161 if (values.IsEmpty()) { 162 nsresult rv = 163 AutoCSSDeclarationAPIWrapper(htmlEditor, styledElement, cssDecl) 164 .RemoveProperty(propertyNameString); 165 if (NS_FAILED(rv)) { 166 NS_WARNING("AutoCSSDeclarationAPIWrapper::RemoveProperty() failed"); 167 return rv; 168 } 169 } else { 170 nsAutoCString priority; 171 cssDecl->GetPropertyPriority(propertyNameString, priority); 172 nsresult rv = 173 AutoCSSDeclarationAPIWrapper(htmlEditor, styledElement, cssDecl) 174 .SetProperty(propertyNameString, values, priority); 175 if (NS_FAILED(rv)) { 176 NS_WARNING("AutoCSSDeclarationAPIWrapper::SetProperty() failed"); 177 return rv; 178 } 179 } 180 } else { 181 nsresult rv = 182 AutoCSSDeclarationAPIWrapper(htmlEditor, styledElement, cssDecl) 183 .RemoveProperty(propertyNameString); 184 if (NS_FAILED(rv)) { 185 NS_WARNING("AutoCSSDeclarationAPIWrapper::RemoveProperty() failed"); 186 return rv; 187 } 188 } 189 } else { 190 nsAutoCString priority; 191 cssDecl->GetPropertyPriority(propertyNameString, priority); 192 if (mProperty == nsGkAtoms::text_decoration) { 193 BuildTextDecorationValueToSet(values, mValue, values); 194 } else { 195 values.Assign(mValue); 196 } 197 nsresult rv = 198 AutoCSSDeclarationAPIWrapper(htmlEditor, styledElement, cssDecl) 199 .SetProperty(propertyNameString, values, priority); 200 if (NS_FAILED(rv)) { 201 NS_WARNING("AutoCSSDeclarationAPIWrapper::SetProperty() failed"); 202 return rv; 203 } 204 } 205 206 // Let's be sure we don't keep an empty style attribute 207 uint32_t length = cssDecl->Length(); 208 if (!length) { 209 AutoElementAttrAPIWrapper elementWrapper(htmlEditor, styledElement); 210 nsresult rv = elementWrapper.UnsetAttr(nsGkAtoms::style, true); 211 if (NS_FAILED(rv)) { 212 NS_WARNING("AutoElementAttrAPIWrapper::UnsetAttr() failed"); 213 return rv; 214 } 215 NS_WARNING_ASSERTION( 216 elementWrapper.IsExpectedResult(EmptyString()), 217 "Removing style attribute caused other mutations, but ignored"); 218 } else { 219 mRedoAttributeWasSet = true; 220 } 221 222 cssDecl->GetPropertyValue(propertyNameString, mRedoValue); 223 return NS_OK; 224 } 225 226 nsresult ChangeStyleTransaction::SetStyle(bool aAttributeWasSet, 227 nsACString& aValue) { 228 if (NS_WARN_IF(!mHTMLEditor) || NS_WARN_IF(!mStyledElement)) { 229 return NS_ERROR_NOT_AVAILABLE; 230 } 231 const OwningNonNull<HTMLEditor> htmlEditor = *mHTMLEditor; 232 const OwningNonNull<nsStyledElement> styledElement = *mStyledElement; 233 if (aAttributeWasSet) { 234 // The style attribute was not empty, let's recreate the declaration 235 nsAutoCString propertyNameString; 236 mProperty->ToUTF8String(propertyNameString); 237 238 const nsCOMPtr<nsDOMCSSDeclaration> cssDecl = styledElement->Style(); 239 240 ErrorResult error; 241 if (aValue.IsEmpty()) { 242 // An empty value means we have to remove the property 243 nsresult rv = 244 AutoCSSDeclarationAPIWrapper(htmlEditor, styledElement, cssDecl) 245 .RemoveProperty(propertyNameString); 246 if (NS_FAILED(rv)) { 247 NS_WARNING("AutoCSSDeclarationAPIWrapper::RemoveProperty() failed"); 248 return rv; 249 } 250 } 251 // Let's recreate the declaration as it was 252 nsAutoCString priority; 253 cssDecl->GetPropertyPriority(propertyNameString, priority); 254 nsresult rv = 255 AutoCSSDeclarationAPIWrapper(htmlEditor, styledElement, cssDecl) 256 .SetProperty(propertyNameString, aValue, priority); 257 if (NS_FAILED(rv)) { 258 NS_WARNING("AutoCSSDeclarationAPIWrapper::SetProperty() failed"); 259 return rv; 260 } 261 return NS_OK; 262 } 263 264 AutoElementAttrAPIWrapper elementWrapper(htmlEditor, styledElement); 265 nsresult rv = elementWrapper.UnsetAttr(nsGkAtoms::style, true); 266 if (NS_FAILED(rv)) { 267 NS_WARNING("AutoElementAttrAPIWrapper::UnsetAttr() failed"); 268 return rv; 269 } 270 NS_WARNING_ASSERTION( 271 elementWrapper.IsExpectedResult(EmptyString()), 272 "Removing style attribute caused other mutations, but ignored"); 273 return NS_OK; 274 } 275 276 NS_IMETHODIMP ChangeStyleTransaction::UndoTransaction() { 277 MOZ_LOG(GetLogModule(), LogLevel::Info, 278 ("%p ChangeStyleTransaction::%s this=%s", this, __FUNCTION__, 279 ToString(*this).c_str())); 280 281 nsresult rv = SetStyle(mUndoAttributeWasSet, mUndoValue); 282 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 283 "ChangeStyleTransaction::SetStyle() failed"); 284 return rv; 285 } 286 287 NS_IMETHODIMP ChangeStyleTransaction::RedoTransaction() { 288 MOZ_LOG(GetLogModule(), LogLevel::Info, 289 ("%p ChangeStyleTransaction::%s this=%s", this, __FUNCTION__, 290 ToString(*this).c_str())); 291 292 nsresult rv = SetStyle(mRedoAttributeWasSet, mRedoValue); 293 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 294 "ChangeStyleTransaction::SetStyle() failed"); 295 return rv; 296 } 297 298 // static 299 void ChangeStyleTransaction::BuildTextDecorationValueToSet( 300 const nsACString& aCurrentValues, const nsACString& aAddingValues, 301 nsACString& aOutValues) { 302 const bool underline = ValueIncludes(aCurrentValues, "underline"_ns) || 303 ValueIncludes(aAddingValues, "underline"_ns); 304 const bool overline = ValueIncludes(aCurrentValues, "overline"_ns) || 305 ValueIncludes(aAddingValues, "overline"_ns); 306 const bool lineThrough = ValueIncludes(aCurrentValues, "line-through"_ns) || 307 ValueIncludes(aAddingValues, "line-through"_ns); 308 // FYI: Don't refer aCurrentValues which may refer same instance as 309 // aOutValues. 310 BuildTextDecorationValue(underline, overline, lineThrough, aOutValues); 311 } 312 313 // static 314 void ChangeStyleTransaction::BuildTextDecorationValueToRemove( 315 const nsACString& aCurrentValues, const nsACString& aRemovingValues, 316 nsACString& aOutValues) { 317 const bool underline = ValueIncludes(aCurrentValues, "underline"_ns) && 318 !ValueIncludes(aRemovingValues, "underline"_ns); 319 const bool overline = ValueIncludes(aCurrentValues, "overline"_ns) && 320 !ValueIncludes(aRemovingValues, "overline"_ns); 321 const bool lineThrough = ValueIncludes(aCurrentValues, "line-through"_ns) && 322 !ValueIncludes(aRemovingValues, "line-through"_ns); 323 // FYI: Don't refer aCurrentValues which may refer same instance as 324 // aOutValues. 325 BuildTextDecorationValue(underline, overline, lineThrough, aOutValues); 326 } 327 328 void ChangeStyleTransaction::BuildTextDecorationValue(bool aUnderline, 329 bool aOverline, 330 bool aLineThrough, 331 nsACString& aOutValues) { 332 // We should build text-decoration(-line) value as same as Blink for 333 // compatibility. Blink sets text-decoration-line to the values in the 334 // following order. Blink drops `blink` and other styles like color and 335 // style. For keeping the code simple, let's use the lossy behavior. 336 aOutValues.Truncate(); 337 if (aUnderline) { 338 aOutValues.AssignLiteral("underline"); 339 } 340 if (aOverline) { 341 if (!aOutValues.IsEmpty()) { 342 aOutValues.Append(HTMLEditUtils::kSpace); 343 } 344 aOutValues.AppendLiteral("overline"); 345 } 346 if (aLineThrough) { 347 if (!aOutValues.IsEmpty()) { 348 aOutValues.Append(HTMLEditUtils::kSpace); 349 } 350 aOutValues.AppendLiteral("line-through"); 351 } 352 } 353 354 } // namespace mozilla