nsCounterManager.cpp (20354B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 /* implementation of CSS counters (for numbering things) */ 8 9 #include "nsCounterManager.h" 10 11 #include "mozilla/AutoRestore.h" 12 #include "mozilla/ContainStyleScopeManager.h" 13 #include "mozilla/Likely.h" 14 #include "mozilla/PresShell.h" 15 #include "mozilla/StaticPrefs_layout.h" 16 #include "mozilla/WritingModes.h" 17 #include "mozilla/dom/Element.h" 18 #include "mozilla/dom/Text.h" 19 #include "nsContainerFrame.h" 20 #include "nsContentUtils.h" 21 #include "nsIContent.h" 22 #include "nsIContentInlines.h" 23 #include "nsIFrame.h" 24 #include "nsTArray.h" 25 26 using namespace mozilla; 27 28 bool nsCounterUseNode::InitTextFrame(nsGenConList* aList, 29 nsIFrame* aPseudoFrame, 30 nsIFrame* aTextFrame) { 31 nsCounterNode::InitTextFrame(aList, aPseudoFrame, aTextFrame); 32 33 auto* counterList = static_cast<nsCounterList*>(aList); 34 counterList->Insert(this); 35 aPseudoFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE); 36 // If the list is already dirty, or the node is not at the end, just start 37 // with an empty string for now and when we recalculate the list we'll change 38 // the value to the right one. 39 if (counterList->IsDirty()) { 40 return false; 41 } 42 if (!counterList->IsLast(this)) { 43 counterList->SetDirty(); 44 return true; 45 } 46 Calc(counterList, /* aNotify = */ false); 47 return false; 48 } 49 50 // assign the correct |mValueAfter| value to a node that has been inserted 51 // Should be called immediately after calling |Insert|. 52 void nsCounterUseNode::Calc(nsCounterList* aList, bool aNotify) { 53 NS_ASSERTION(aList->IsRecalculatingAll() || !aList->IsDirty(), 54 "Why are we calculating with a dirty list?"); 55 56 mValueAfter = nsCounterList::ValueBefore(this); 57 58 if (mText) { 59 nsAutoString contentString; 60 GetText(contentString); 61 mText->SetText(contentString, aNotify); 62 } 63 } 64 65 // assign the correct |mValueAfter| value to a node that has been inserted 66 // Should be called immediately after calling |Insert|. 67 void nsCounterChangeNode::Calc(nsCounterList* aList) { 68 NS_ASSERTION(aList->IsRecalculatingAll() || !aList->IsDirty(), 69 "Why are we calculating with a dirty list?"); 70 if (IsContentBasedReset()) { 71 // RecalcAll takes care of this case. 72 } else if (mType == RESET || mType == SET) { 73 mValueAfter = mChangeValue; 74 } else { 75 NS_ASSERTION(mType == INCREMENT, "invalid type"); 76 mValueAfter = nsCounterManager::IncrementCounter( 77 nsCounterList::ValueBefore(this), mChangeValue); 78 } 79 } 80 81 void nsCounterUseNode::GetText(nsString& aResult) { 82 mPseudoFrame->PresContext() 83 ->CounterStyleManager() 84 ->WithCounterStyleNameOrSymbols(mCounterStyle, [&](CounterStyle* aStyle) { 85 GetText(mPseudoFrame->GetWritingMode(), aStyle, aResult); 86 }); 87 } 88 89 void nsCounterUseNode::GetText(WritingMode aWM, CounterStyle* aStyle, 90 nsString& aResult) { 91 const bool isBidiRTL = aWM.IsBidiRTL(); 92 auto AppendCounterText = [&aResult, isBidiRTL](const nsAutoString& aText, 93 bool aIsRTL) { 94 if (MOZ_LIKELY(isBidiRTL == aIsRTL)) { 95 aResult.Append(aText); 96 } else { 97 // RLM = 0x200f, LRM = 0x200e 98 const char16_t mark = aIsRTL ? 0x200f : 0x200e; 99 aResult.Append(mark); 100 aResult.Append(aText); 101 aResult.Append(mark); 102 } 103 }; 104 105 if (mForLegacyBullet) { 106 nsAutoString prefix; 107 aStyle->GetPrefix(prefix); 108 aResult.Assign(prefix); 109 } 110 111 AutoTArray<nsCounterNode*, 8> stack; 112 stack.AppendElement(static_cast<nsCounterNode*>(this)); 113 114 if (mAllCounters && mScopeStart) { 115 for (nsCounterNode* n = mScopeStart; n->mScopePrev; n = n->mScopeStart) { 116 stack.AppendElement(n->mScopePrev); 117 } 118 } 119 120 for (nsCounterNode* n : Reversed(stack)) { 121 nsAutoString text; 122 bool isTextRTL; 123 aStyle->GetCounterText(n->mValueAfter, aWM, text, isTextRTL); 124 if (!mForLegacyBullet || aStyle->IsBullet()) { 125 aResult.Append(text); 126 } else { 127 AppendCounterText(text, isTextRTL); 128 } 129 if (n == this) { 130 break; 131 } 132 aResult.Append(mSeparator); 133 } 134 135 if (mForLegacyBullet) { 136 nsAutoString suffix; 137 aStyle->GetSuffix(suffix); 138 aResult.Append(suffix); 139 } 140 } 141 142 static const nsIContent* GetParentContentForScope(nsIFrame* frame) { 143 // We do not want elements with `display: contents` to establish scope for 144 // counters. We'd like to do something like 145 // `nsIFrame::GetClosestFlattenedTreeAncestorPrimaryFrame()` above, but this 146 // may be called before the primary frame is set on frames. 147 nsIContent* content = frame->GetContent()->GetFlattenedTreeParent(); 148 while (content && content->IsElement() && 149 content->AsElement()->IsDisplayContents()) { 150 content = content->GetFlattenedTreeParent(); 151 } 152 153 return content; 154 } 155 156 bool nsCounterList::IsDirty() const { 157 return mScope->GetScopeManager().CounterDirty(mCounterName); 158 } 159 160 void nsCounterList::SetDirty() { 161 mScope->GetScopeManager().SetCounterDirty(mCounterName); 162 } 163 164 void nsCounterList::SetScope(nsCounterNode* aNode) { 165 // This function is responsible for setting |mScopeStart| and 166 // |mScopePrev| (whose purpose is described in nsCounterManager.h). 167 // We do this by starting from the node immediately preceding 168 // |aNode| in content tree order, which is reasonably likely to be 169 // the previous element in our scope (or, for a reset, the previous 170 // element in the containing scope, which is what we want). If 171 // we're not in the same scope that it is, then it's too deep in the 172 // frame tree, so we walk up parent scopes until we find something 173 // appropriate. 174 175 auto setNullScopeFor = [](nsCounterNode* aNode) { 176 aNode->mScopeStart = nullptr; 177 aNode->mScopePrev = nullptr; 178 aNode->mCrossesContainStyleBoundaries = false; 179 if (aNode->IsUnitializedIncrementNode()) { 180 aNode->ChangeNode()->mChangeValue = 1; 181 } 182 }; 183 184 if (aNode == First() && aNode->mType != nsCounterNode::USE) { 185 setNullScopeFor(aNode); 186 return; 187 } 188 189 auto didSetScopeFor = [this](nsCounterNode* aNode) { 190 if (aNode->mType == nsCounterNode::USE) { 191 return; 192 } 193 if (aNode->mScopeStart->IsContentBasedReset()) { 194 SetDirty(); 195 } 196 if (aNode->IsUnitializedIncrementNode()) { 197 aNode->ChangeNode()->mChangeValue = 198 aNode->mScopeStart->IsReversed() ? -1 : 1; 199 } 200 }; 201 202 // If there exist an explicit RESET scope created by an ancestor or 203 // the element itself, then we use that scope. 204 // Otherwise, fall through to consider scopes created by siblings (and 205 // their descendants) in reverse document order. 206 // Do this only for the list-item counter, while the CSSWG discusses what the 207 // right thing to do here is, see bug 1548753 and 208 // https://github.com/w3c/csswg-drafts/issues/5477. 209 if (mCounterName == nsGkAtoms::list_item && 210 aNode->mType != nsCounterNode::USE && 211 StaticPrefs::layout_css_counter_ancestor_scope_enabled()) { 212 for (auto* p = aNode->mPseudoFrame; p; p = p->GetParent()) { 213 // This relies on the fact that a RESET node is always the first 214 // CounterNode for a frame if it has any. 215 auto* counter = GetFirstNodeFor(p); 216 if (!counter || counter->mType != nsCounterNode::RESET) { 217 continue; 218 } 219 if (p == aNode->mPseudoFrame) { 220 break; 221 } 222 aNode->mScopeStart = counter; 223 aNode->mScopePrev = counter; 224 aNode->mCrossesContainStyleBoundaries = false; 225 for (nsCounterNode* prev = Prev(aNode); prev; prev = prev->mScopePrev) { 226 if (prev->mScopeStart == counter) { 227 aNode->mScopePrev = 228 prev->mType == nsCounterNode::RESET ? prev->mScopePrev : prev; 229 break; 230 } 231 if (prev->mType != nsCounterNode::RESET) { 232 prev = prev->mScopeStart; 233 if (!prev) { 234 break; 235 } 236 } 237 } 238 didSetScopeFor(aNode); 239 return; 240 } 241 } 242 243 // Get the content node for aNode's rendering object's *parent*, 244 // since scope includes siblings, so we want a descendant check on 245 // parents. Note here that mPseudoFrame is a bit of a misnomer, as it 246 // might not be a pseudo element at all, but a normal element that 247 // happens to increment a counter. We want to respect the flat tree 248 // here, but skipping any <slot> element that happens to contain 249 // mPseudoFrame. That's why this uses GetInFlowParent() instead 250 // of GetFlattenedTreeParent(). 251 const nsIContent* nodeContent = GetParentContentForScope(aNode->mPseudoFrame); 252 if (SetScopeByWalkingBackwardThroughList(aNode, nodeContent, Prev(aNode))) { 253 aNode->mCrossesContainStyleBoundaries = false; 254 didSetScopeFor(aNode); 255 return; 256 } 257 258 // If this is a USE node there's a possibility that its counter scope starts 259 // in a parent `contain: style` scope. Look upward in the `contain: style` 260 // scope tree to find an appropriate node with which this node shares a 261 // counter scope. 262 if (aNode->mType == nsCounterNode::USE && aNode == First()) { 263 for (auto* scope = mScope->GetParent(); scope; scope = scope->GetParent()) { 264 if (auto* counterList = 265 scope->GetCounterManager().GetCounterList(mCounterName)) { 266 if (auto* node = static_cast<nsCounterNode*>( 267 mScope->GetPrecedingElementInGenConList(counterList))) { 268 if (SetScopeByWalkingBackwardThroughList(aNode, nodeContent, node)) { 269 aNode->mCrossesContainStyleBoundaries = true; 270 didSetScopeFor(aNode); 271 return; 272 } 273 } 274 } 275 } 276 } 277 278 setNullScopeFor(aNode); 279 } 280 281 bool nsCounterList::SetScopeByWalkingBackwardThroughList( 282 nsCounterNode* aNodeToSetScopeFor, const nsIContent* aNodeContent, 283 nsCounterNode* aNodeToBeginLookingAt) { 284 for (nsCounterNode *prev = aNodeToBeginLookingAt, *start; prev; 285 prev = start->mScopePrev) { 286 // There are two possibilities here: 287 // 1. |prev| starts a new counter scope. This happens when: 288 // a. It's a reset node. 289 // b. It's an implied reset node which we know because mScopeStart is null. 290 // c. It follows one or more USE nodes at the start of the list which have 291 // a scope that starts in a parent `contain: style` context. 292 // In all of these cases, |prev| should be the start of this node's counter 293 // scope. 294 // 2. |prev| does not start a new counter scope and this node should share a 295 // counter scope start with |prev|. 296 start = 297 (prev->mType == nsCounterNode::RESET || !prev->mScopeStart || 298 (prev->mScopePrev && prev->mScopePrev->mCrossesContainStyleBoundaries)) 299 ? prev 300 : prev->mScopeStart; 301 302 const nsIContent* startContent = 303 GetParentContentForScope(start->mPseudoFrame); 304 NS_ASSERTION(aNodeContent || !startContent, 305 "null check on startContent should be sufficient to " 306 "null check aNodeContent as well, since if aNodeContent " 307 "is for the root, startContent (which is before it) " 308 "must be too"); 309 310 // A reset's outer scope can't be a scope created by a sibling. 311 if (!(aNodeToSetScopeFor->mType == nsCounterNode::RESET && 312 aNodeContent == startContent) && 313 // everything is inside the root (except the case above, 314 // a second reset on the root) 315 (!startContent || 316 aNodeContent->IsInclusiveFlatTreeDescendantOf(startContent))) { 317 // If this node is a USE node and the previous node was also a USE node 318 // which has a scope that starts in a parent `contain: style` context, 319 // this node's scope shares the same scope and crosses `contain: style` 320 // scope boundaries. 321 if (aNodeToSetScopeFor->mType == nsCounterNode::USE) { 322 aNodeToSetScopeFor->mCrossesContainStyleBoundaries = 323 prev->mCrossesContainStyleBoundaries; 324 } 325 326 aNodeToSetScopeFor->mScopeStart = start; 327 aNodeToSetScopeFor->mScopePrev = prev; 328 return true; 329 } 330 } 331 332 return false; 333 } 334 335 #if defined(DEBUG) || defined(MOZ_LAYOUT_DEBUGGER) 336 void nsCounterList::Dump() { 337 int32_t i = 0; 338 for (auto* node = First(); node; node = Next(node)) { 339 const char* types[] = {"RESET", "INCREMENT", "SET", "USE"}; 340 printf( 341 " Node #%d @%p frame=%p index=%d type=%s valAfter=%d\n" 342 " scope-start=%p scope-prev=%p", 343 i++, (void*)node, (void*)node->mPseudoFrame, node->mContentIndex, 344 types[node->mType], node->mValueAfter, (void*)node->mScopeStart, 345 (void*)node->mScopePrev); 346 if (node->mType == nsCounterNode::USE) { 347 nsAutoString text; 348 node->UseNode()->GetText(text); 349 printf(" text=%s", NS_ConvertUTF16toUTF8(text).get()); 350 } 351 printf("\n"); 352 } 353 } 354 #endif 355 356 void nsCounterList::RecalcAll() { 357 AutoRestore<bool> restoreRecalculatingAll(mRecalculatingAll); 358 mRecalculatingAll = true; 359 360 // Setup the scope and calculate the default start value for content-based 361 // reversed() counters. We need to track the last increment for each of 362 // those scopes so that we can add it in an extra time at the end. 363 // https://drafts.csswg.org/css-lists/#instantiating-counters 364 nsTHashMap<nsPtrHashKey<nsCounterChangeNode>, int32_t> scopes; 365 for (nsCounterNode* node = First(); node; node = Next(node)) { 366 SetScope(node); 367 if (node->IsContentBasedReset()) { 368 node->ChangeNode()->mSeenSetNode = false; 369 node->mValueAfter = 0; 370 scopes.InsertOrUpdate(node->ChangeNode(), 0); 371 } else if (node->mScopeStart && node->mScopeStart->IsContentBasedReset() && 372 !node->mScopeStart->ChangeNode()->mSeenSetNode) { 373 if (node->mType == nsCounterChangeNode::INCREMENT) { 374 auto incrementNegated = -node->ChangeNode()->mChangeValue; 375 if (auto entry = scopes.Lookup(node->mScopeStart->ChangeNode())) { 376 entry.Data() = incrementNegated; 377 } 378 auto* next = Next(node); 379 if (next && next->mPseudoFrame == node->mPseudoFrame && 380 next->mType == nsCounterChangeNode::SET) { 381 continue; 382 } 383 node->mScopeStart->mValueAfter += incrementNegated; 384 } else if (node->mType == nsCounterChangeNode::SET) { 385 node->mScopeStart->mValueAfter += node->ChangeNode()->mChangeValue; 386 // We have a 'counter-set' for this scope so we're done. 387 // The counter is incremented from that value for the remaining nodes. 388 node->mScopeStart->ChangeNode()->mSeenSetNode = true; 389 } 390 } 391 } 392 393 // For all the content-based reversed() counters we found, add in the 394 // incrementNegated from its last counter-increment. 395 for (auto iter = scopes.ConstIter(); !iter.Done(); iter.Next()) { 396 iter.Key()->mValueAfter += iter.Data(); 397 } 398 399 for (nsCounterNode* node = First(); node; node = Next(node)) { 400 node->Calc(this, /* aNotify = */ true); 401 } 402 } 403 404 static bool AddCounterChangeNode(nsCounterManager& aManager, nsIFrame* aFrame, 405 int32_t aIndex, 406 const nsStyleContent::CounterPair& aPair, 407 nsCounterNode::Type aType) { 408 auto* node = new nsCounterChangeNode(aFrame, aType, aPair.value, aIndex, 409 aPair.is_reversed); 410 nsCounterList* counterList = 411 aManager.GetOrCreateCounterList(aPair.name.AsAtom()); 412 counterList->Insert(node); 413 if (!counterList->IsLast(node)) { 414 // Tell the caller it's responsible for recalculating the entire list. 415 counterList->SetDirty(); 416 return true; 417 } 418 419 // Don't call Calc() if the list is already dirty -- it'll be recalculated 420 // anyway, and trying to calculate with a dirty list doesn't work. 421 if (MOZ_LIKELY(!counterList->IsDirty())) { 422 node->Calc(counterList); 423 } 424 return counterList->IsDirty(); 425 } 426 427 static bool HasCounters(const nsStyleContent& aStyle) { 428 return !aStyle.mCounterIncrement.IsEmpty() || 429 !aStyle.mCounterReset.IsEmpty() || !aStyle.mCounterSet.IsEmpty(); 430 } 431 432 bool nsCounterManager::AddCounterChanges(nsIFrame* aFrame) { 433 // For elements with 'display:list-item' we add a default 434 // 'counter-increment:list-item' unless 'counter-increment' already has a 435 // value for 'list-item'. 436 // 437 // https://drafts.csswg.org/css-lists-3/#declaring-a-list-item 438 // 439 // We inherit `display` for some anonymous boxes, but we don't want them to 440 // increment the list-item counter. 441 const bool requiresListItemIncrement = 442 aFrame->StyleDisplay()->IsListItem() && !aFrame->Style()->IsAnonBox(); 443 444 const nsStyleContent* styleContent = aFrame->StyleContent(); 445 446 if (!requiresListItemIncrement && !HasCounters(*styleContent)) { 447 MOZ_ASSERT(!aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE)); 448 return false; 449 } 450 451 aFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE); 452 453 bool dirty = false; 454 // Add in order, resets first, so all the comparisons will be optimized 455 // for addition at the end of the list. 456 { 457 int32_t i = 0; 458 for (const auto& pair : styleContent->mCounterReset.AsSpan()) { 459 dirty |= AddCounterChangeNode(*this, aFrame, i++, pair, 460 nsCounterChangeNode::RESET); 461 } 462 } 463 bool hasListItemIncrement = false; 464 { 465 int32_t i = 0; 466 for (const auto& pair : styleContent->mCounterIncrement.AsSpan()) { 467 hasListItemIncrement |= pair.name.AsAtom() == nsGkAtoms::list_item; 468 if (pair.value != 0) { 469 dirty |= AddCounterChangeNode(*this, aFrame, i++, pair, 470 nsCounterChangeNode::INCREMENT); 471 } 472 } 473 } 474 475 if (requiresListItemIncrement && !hasListItemIncrement) { 476 RefPtr<nsAtom> atom = nsGkAtoms::list_item; 477 // We use a magic value here to signal to SetScope() that it should 478 // set the value to -1 or 1 depending on if the scope is reversed() 479 // or not. 480 auto listItemIncrement = nsStyleContent::CounterPair{ 481 {StyleAtom(atom.forget())}, std::numeric_limits<int32_t>::min()}; 482 dirty |= AddCounterChangeNode( 483 *this, aFrame, styleContent->mCounterIncrement.Length(), 484 listItemIncrement, nsCounterChangeNode::INCREMENT); 485 } 486 487 { 488 int32_t i = 0; 489 for (const auto& pair : styleContent->mCounterSet.AsSpan()) { 490 dirty |= AddCounterChangeNode(*this, aFrame, i++, pair, 491 nsCounterChangeNode::SET); 492 } 493 } 494 return dirty; 495 } 496 497 nsCounterList* nsCounterManager::GetOrCreateCounterList(nsAtom* aCounterName) { 498 MOZ_ASSERT(aCounterName); 499 return mNames.GetOrInsertNew(aCounterName, aCounterName, mScope); 500 } 501 502 nsCounterList* nsCounterManager::GetCounterList(nsAtom* aCounterName) { 503 MOZ_ASSERT(aCounterName); 504 return mNames.Get(aCounterName); 505 } 506 507 void nsCounterManager::RecalcAll() { 508 for (const auto& list : mNames.Values()) { 509 if (list->IsDirty()) { 510 list->RecalcAll(); 511 } 512 } 513 } 514 515 void nsCounterManager::SetAllDirty() { 516 for (const auto& list : mNames.Values()) { 517 list->SetDirty(); 518 } 519 } 520 521 bool nsCounterManager::DestroyNodesFor(nsIFrame* aFrame) { 522 MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE), 523 "why call me?"); 524 bool destroyedAny = false; 525 for (const auto& list : mNames.Values()) { 526 if (list->DestroyNodesFor(aFrame)) { 527 destroyedAny = true; 528 list->SetDirty(); 529 } 530 } 531 return destroyedAny; 532 } 533 534 #ifdef ACCESSIBILITY 535 bool nsCounterManager::GetFirstCounterValueForFrame( 536 nsIFrame* aFrame, CounterValue& aOrdinal) const { 537 if (const auto* list = mNames.Get(nsGkAtoms::list_item)) { 538 for (nsCounterNode* n = list->GetFirstNodeFor(aFrame); 539 n && n->mPseudoFrame == aFrame; n = list->Next(n)) { 540 if (n->mType == nsCounterNode::USE) { 541 aOrdinal = n->mValueAfter; 542 return true; 543 } 544 } 545 } 546 547 return false; 548 } 549 #endif 550 551 #if defined(DEBUG) || defined(MOZ_LAYOUT_DEBUGGER) 552 void nsCounterManager::Dump() const { 553 printf("\n\nCounter Manager Lists:\n"); 554 for (const auto& entry : mNames) { 555 printf("Counter named \"%s\":\n", nsAtomCString(entry.GetKey()).get()); 556 557 nsCounterList* list = entry.GetWeak(); 558 list->Dump(); 559 } 560 printf("\n\n"); 561 } 562 #endif