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/Likely.h"
12 #include "mozilla/IntegerRange.h"
13 #include "mozilla/PresShell.h"
14 #include "mozilla/StaticPrefs_layout.h"
15 #include "mozilla/WritingModes.h"
16 #include "nsContentUtils.h"
17 #include "nsIContent.h"
18 #include "nsTArray.h"
19 #include "mozilla/dom/Text.h"
20
21 using namespace mozilla;
22
InitTextFrame(nsGenConList * aList,nsIFrame * aPseudoFrame,nsIFrame * aTextFrame)23 bool nsCounterUseNode::InitTextFrame(nsGenConList* aList,
24 nsIFrame* aPseudoFrame,
25 nsIFrame* aTextFrame) {
26 nsCounterNode::InitTextFrame(aList, aPseudoFrame, aTextFrame);
27
28 auto* counterList = static_cast<nsCounterList*>(aList);
29 counterList->Insert(this);
30 aPseudoFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE);
31 // If the list is already dirty, or the node is not at the end, just start
32 // with an empty string for now and when we recalculate the list we'll change
33 // the value to the right one.
34 if (counterList->IsDirty()) {
35 return false;
36 }
37 if (!counterList->IsLast(this)) {
38 counterList->SetDirty();
39 return true;
40 }
41 Calc(counterList, /* aNotify = */ false);
42 return false;
43 }
44
45 // assign the correct |mValueAfter| value to a node that has been inserted
46 // Should be called immediately after calling |Insert|.
Calc(nsCounterList * aList,bool aNotify)47 void nsCounterUseNode::Calc(nsCounterList* aList, bool aNotify) {
48 NS_ASSERTION(!aList->IsDirty(), "Why are we calculating with a dirty list?");
49 mValueAfter = nsCounterList::ValueBefore(this);
50 if (mText) {
51 nsAutoString contentString;
52 GetText(contentString);
53 mText->SetText(contentString, aNotify);
54 }
55 }
56
57 // assign the correct |mValueAfter| value to a node that has been inserted
58 // Should be called immediately after calling |Insert|.
Calc(nsCounterList * aList)59 void nsCounterChangeNode::Calc(nsCounterList* aList) {
60 NS_ASSERTION(!aList->IsDirty(), "Why are we calculating with a dirty list?");
61 if (IsContentBasedReset()) {
62 // RecalcAll takes care of this case.
63 } else if (mType == RESET || mType == SET) {
64 mValueAfter = mChangeValue;
65 } else {
66 NS_ASSERTION(mType == INCREMENT, "invalid type");
67 mValueAfter = nsCounterManager::IncrementCounter(
68 nsCounterList::ValueBefore(this), mChangeValue);
69 }
70 }
71
GetText(nsString & aResult)72 void nsCounterUseNode::GetText(nsString& aResult) {
73 CounterStyle* style =
74 mPseudoFrame->PresContext()->CounterStyleManager()->ResolveCounterStyle(
75 mCounterStyle);
76 GetText(mPseudoFrame->GetWritingMode(), style, aResult);
77 }
78
GetText(WritingMode aWM,CounterStyle * aStyle,nsString & aResult)79 void nsCounterUseNode::GetText(WritingMode aWM, CounterStyle* aStyle,
80 nsString& aResult) {
81 const bool isBidiRTL = aWM.IsBidiRTL();
82 auto AppendCounterText = [&aResult, isBidiRTL](const nsAutoString& aText,
83 bool aIsRTL) {
84 if (MOZ_LIKELY(isBidiRTL == aIsRTL)) {
85 aResult.Append(aText);
86 } else {
87 // RLM = 0x200f, LRM = 0x200e
88 const char16_t mark = aIsRTL ? 0x200f : 0x200e;
89 aResult.Append(mark);
90 aResult.Append(aText);
91 aResult.Append(mark);
92 }
93 };
94
95 if (mForLegacyBullet) {
96 nsAutoString prefix;
97 aStyle->GetPrefix(prefix);
98 aResult.Assign(prefix);
99 }
100
101 AutoTArray<nsCounterNode*, 8> stack;
102 stack.AppendElement(static_cast<nsCounterNode*>(this));
103
104 if (mAllCounters && mScopeStart) {
105 for (nsCounterNode* n = mScopeStart; n->mScopePrev; n = n->mScopeStart) {
106 stack.AppendElement(n->mScopePrev);
107 }
108 }
109
110 for (nsCounterNode* n : Reversed(stack)) {
111 nsAutoString text;
112 bool isTextRTL;
113 aStyle->GetCounterText(n->mValueAfter, aWM, text, isTextRTL);
114 if (!mForLegacyBullet || aStyle->IsBullet()) {
115 aResult.Append(text);
116 } else {
117 AppendCounterText(text, isTextRTL);
118 }
119 if (n == this) {
120 break;
121 }
122 aResult.Append(mSeparator);
123 }
124
125 if (mForLegacyBullet) {
126 nsAutoString suffix;
127 aStyle->GetSuffix(suffix);
128 aResult.Append(suffix);
129 }
130 }
131
SetScope(nsCounterNode * aNode)132 void nsCounterList::SetScope(nsCounterNode* aNode) {
133 // This function is responsible for setting |mScopeStart| and
134 // |mScopePrev| (whose purpose is described in nsCounterManager.h).
135 // We do this by starting from the node immediately preceding
136 // |aNode| in content tree order, which is reasonably likely to be
137 // the previous element in our scope (or, for a reset, the previous
138 // element in the containing scope, which is what we want). If
139 // we're not in the same scope that it is, then it's too deep in the
140 // frame tree, so we walk up parent scopes until we find something
141 // appropriate.
142
143 if (aNode == First()) {
144 aNode->mScopeStart = nullptr;
145 aNode->mScopePrev = nullptr;
146 return;
147 }
148
149 // If there exist an explicit RESET scope created by an ancestor or
150 // the element itself, then we use that scope.
151 // Otherwise, fall through to consider scopes created by siblings (and
152 // their descendants) in reverse document order.
153 if (aNode->mType != nsCounterNode::USE &&
154 StaticPrefs::layout_css_counter_ancestor_scope_enabled()) {
155 nsIContent* const counterNode = aNode->mPseudoFrame->GetContent();
156 nsCounterNode* lastPrev = nullptr;
157 for (nsCounterNode* prev = Prev(aNode); prev; prev = prev->mScopePrev) {
158 if (prev->mType == nsCounterNode::RESET) {
159 if (aNode->mPseudoFrame == prev->mPseudoFrame) {
160 break;
161 }
162 // FIXME(bug 1477524): should use flattened tree here:
163 nsIContent* resetNode = prev->mPseudoFrame->GetContent();
164 if (counterNode->IsInclusiveDescendantOf(resetNode)) {
165 aNode->mScopeStart = prev;
166 aNode->mScopePrev = lastPrev ? lastPrev : prev;
167 return;
168 }
169 lastPrev = prev->mScopePrev;
170 } else if (!lastPrev) {
171 lastPrev = prev;
172 }
173 }
174 }
175
176 // Get the content node for aNode's rendering object's *parent*,
177 // since scope includes siblings, so we want a descendant check on
178 // parents.
179 nsIContent* nodeContent = aNode->mPseudoFrame->GetContent()->GetParent();
180
181 for (nsCounterNode *prev = Prev(aNode), *start; prev;
182 prev = start->mScopePrev) {
183 // If |prev| starts a scope (because it's a real or implied
184 // reset), we want it as the scope start rather than the start
185 // of its enclosing scope. Otherwise, there's no enclosing
186 // scope, so the next thing in prev's scope shares its scope
187 // start.
188 start = (prev->mType == nsCounterNode::RESET || !prev->mScopeStart)
189 ? prev
190 : prev->mScopeStart;
191
192 // |startContent| is analogous to |nodeContent| (see above).
193 nsIContent* startContent = start->mPseudoFrame->GetContent()->GetParent();
194 NS_ASSERTION(nodeContent || !startContent,
195 "null check on startContent should be sufficient to "
196 "null check nodeContent as well, since if nodeContent "
197 "is for the root, startContent (which is before it) "
198 "must be too");
199
200 // A reset's outer scope can't be a scope created by a sibling.
201 if (!(aNode->mType == nsCounterNode::RESET &&
202 nodeContent == startContent) &&
203 // everything is inside the root (except the case above,
204 // a second reset on the root)
205 // FIXME(bug 1477524): should use flattened tree here:
206 (!startContent || nodeContent->IsInclusiveDescendantOf(startContent))) {
207 aNode->mScopeStart = start;
208 aNode->mScopePrev = prev;
209 return;
210 }
211 }
212
213 aNode->mScopeStart = nullptr;
214 aNode->mScopePrev = nullptr;
215 }
216
RecalcAll()217 void nsCounterList::RecalcAll() {
218 mDirty = false;
219
220 // Setup the scope and calculate the default start value for <ol reversed>.
221 for (nsCounterNode* node = First(); node; node = Next(node)) {
222 SetScope(node);
223 if (node->IsContentBasedReset()) {
224 node->mValueAfter = 1;
225 } else if (node->mType == nsCounterChangeNode::INCREMENT &&
226 node->mScopeStart && node->mScopeStart->IsContentBasedReset() &&
227 node->mPseudoFrame->StyleDisplay()->IsListItem()) {
228 ++node->mScopeStart->mValueAfter;
229 }
230 }
231
232 for (nsCounterNode* node = First(); node; node = Next(node)) {
233 node->Calc(this, /* aNotify = */ true);
234 }
235 }
236
AddCounterChangeNode(nsCounterManager & aManager,nsIFrame * aFrame,int32_t aIndex,const nsStyleContent::CounterPair & aPair,nsCounterNode::Type aType)237 static bool AddCounterChangeNode(nsCounterManager& aManager, nsIFrame* aFrame,
238 int32_t aIndex,
239 const nsStyleContent::CounterPair& aPair,
240 nsCounterNode::Type aType) {
241 auto* node = new nsCounterChangeNode(aFrame, aType, aPair.value, aIndex);
242 nsCounterList* counterList = aManager.CounterListFor(aPair.name.AsAtom());
243 counterList->Insert(node);
244 if (!counterList->IsLast(node)) {
245 // Tell the caller it's responsible for recalculating the entire
246 // list.
247 counterList->SetDirty();
248 return true;
249 }
250
251 // Don't call Calc() if the list is already dirty -- it'll be recalculated
252 // anyway, and trying to calculate with a dirty list doesn't work.
253 if (MOZ_LIKELY(!counterList->IsDirty())) {
254 node->Calc(counterList);
255 }
256 return false;
257 }
258
HasCounters(const nsStyleContent & aStyle)259 static bool HasCounters(const nsStyleContent& aStyle) {
260 return !aStyle.mCounterIncrement.IsEmpty() ||
261 !aStyle.mCounterReset.IsEmpty() || !aStyle.mCounterSet.IsEmpty();
262 }
263
AddCounterChanges(nsIFrame * aFrame)264 bool nsCounterManager::AddCounterChanges(nsIFrame* aFrame) {
265 // For elements with 'display:list-item' we add a default
266 // 'counter-increment:list-item' unless 'counter-increment' already has a
267 // value for 'list-item'.
268 //
269 // https://drafts.csswg.org/css-lists-3/#declaring-a-list-item
270 //
271 // We inherit `display` for some anonymous boxes, but we don't want them to
272 // increment the list-item counter.
273 const bool requiresListItemIncrement =
274 aFrame->StyleDisplay()->IsListItem() && !aFrame->Style()->IsAnonBox();
275
276 const nsStyleContent* styleContent = aFrame->StyleContent();
277
278 if (!requiresListItemIncrement && !HasCounters(*styleContent)) {
279 MOZ_ASSERT(!aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE));
280 return false;
281 }
282
283 aFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE);
284
285 bool dirty = false;
286 // Add in order, resets first, so all the comparisons will be optimized
287 // for addition at the end of the list.
288 {
289 int32_t i = 0;
290 for (const auto& pair : styleContent->mCounterReset.AsSpan()) {
291 dirty |= AddCounterChangeNode(*this, aFrame, i++, pair,
292 nsCounterChangeNode::RESET);
293 }
294 }
295 bool hasListItemIncrement = false;
296 {
297 int32_t i = 0;
298 for (const auto& pair : styleContent->mCounterIncrement.AsSpan()) {
299 hasListItemIncrement |= pair.name.AsAtom() == nsGkAtoms::list_item;
300 if (pair.value != 0) {
301 dirty |= AddCounterChangeNode(*this, aFrame, i++, pair,
302 nsCounterChangeNode::INCREMENT);
303 }
304 }
305 }
306
307 if (requiresListItemIncrement && !hasListItemIncrement) {
308 bool reversed =
309 aFrame->StyleList()->mMozListReversed == StyleMozListReversed::True;
310 RefPtr<nsAtom> atom = nsGkAtoms::list_item;
311 auto listItemIncrement = nsStyleContent::CounterPair{
312 {StyleAtom(atom.forget())}, reversed ? -1 : 1};
313 dirty |= AddCounterChangeNode(
314 *this, aFrame, styleContent->mCounterIncrement.Length(),
315 listItemIncrement, nsCounterChangeNode::INCREMENT);
316 }
317
318 {
319 int32_t i = 0;
320 for (const auto& pair : styleContent->mCounterSet.AsSpan()) {
321 dirty |= AddCounterChangeNode(*this, aFrame, i++, pair,
322 nsCounterChangeNode::SET);
323 }
324 }
325 return dirty;
326 }
327
CounterListFor(nsAtom * aCounterName)328 nsCounterList* nsCounterManager::CounterListFor(nsAtom* aCounterName) {
329 MOZ_ASSERT(aCounterName);
330 return mNames.GetOrInsertNew(aCounterName);
331 }
332
RecalcAll()333 void nsCounterManager::RecalcAll() {
334 for (const auto& list : mNames.Values()) {
335 if (list->IsDirty()) {
336 list->RecalcAll();
337 }
338 }
339 }
340
SetAllDirty()341 void nsCounterManager::SetAllDirty() {
342 for (const auto& list : mNames.Values()) {
343 list->SetDirty();
344 }
345 }
346
DestroyNodesFor(nsIFrame * aFrame)347 bool nsCounterManager::DestroyNodesFor(nsIFrame* aFrame) {
348 MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE),
349 "why call me?");
350 bool destroyedAny = false;
351 for (const auto& list : mNames.Values()) {
352 if (list->DestroyNodesFor(aFrame)) {
353 destroyedAny = true;
354 list->SetDirty();
355 }
356 }
357 return destroyedAny;
358 }
359
360 #ifdef ACCESSIBILITY
GetSpokenCounterText(nsIFrame * aFrame,nsAString & aText) const361 void nsCounterManager::GetSpokenCounterText(nsIFrame* aFrame,
362 nsAString& aText) const {
363 CounterValue ordinal = 1;
364 if (const auto* list = mNames.Get(nsGkAtoms::list_item)) {
365 for (nsCounterNode* n = list->GetFirstNodeFor(aFrame);
366 n && n->mPseudoFrame == aFrame; n = list->Next(n)) {
367 if (n->mType == nsCounterNode::USE) {
368 ordinal = n->mValueAfter;
369 break;
370 }
371 }
372 }
373 CounterStyle* counterStyle =
374 aFrame->PresContext()->CounterStyleManager()->ResolveCounterStyle(
375 aFrame->StyleList()->mCounterStyle);
376 nsAutoString text;
377 bool isBullet;
378 counterStyle->GetSpokenCounterText(ordinal, aFrame->GetWritingMode(), text,
379 isBullet);
380 if (isBullet) {
381 aText = text;
382 if (!counterStyle->IsNone()) {
383 aText.Append(' ');
384 }
385 } else {
386 counterStyle->GetPrefix(aText);
387 aText += text;
388 nsAutoString suffix;
389 counterStyle->GetSuffix(suffix);
390 aText += suffix;
391 }
392 }
393 #endif
394
395 #ifdef DEBUG
Dump()396 void nsCounterManager::Dump() {
397 printf("\n\nCounter Manager Lists:\n");
398 for (const auto& entry : mNames) {
399 printf("Counter named \"%s\":\n", nsAtomCString(entry.GetKey()).get());
400
401 nsCounterList* list = entry.GetWeak();
402 int32_t i = 0;
403 for (nsCounterNode* node = list->First(); node; node = list->Next(node)) {
404 const char* types[] = {"RESET", "INCREMENT", "SET", "USE"};
405 printf(
406 " Node #%d @%p frame=%p index=%d type=%s valAfter=%d\n"
407 " scope-start=%p scope-prev=%p",
408 i++, (void*)node, (void*)node->mPseudoFrame, node->mContentIndex,
409 types[node->mType], node->mValueAfter, (void*)node->mScopeStart,
410 (void*)node->mScopePrev);
411 if (node->mType == nsCounterNode::USE) {
412 nsAutoString text;
413 node->UseNode()->GetText(text);
414 printf(" text=%s", NS_ConvertUTF16toUTF8(text).get());
415 }
416 printf("\n");
417 }
418 }
419 printf("\n\n");
420 }
421 #endif
422