1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h"
6 
7 #include <type_traits>
8 
9 #include "third_party/blink/renderer/core/layout/layout_inline.h"
10 #include "third_party/blink/renderer/core/layout/layout_text.h"
11 #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h"
12 #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h"
13 #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h"
14 #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.h"
15 #include "third_party/blink/renderer/core/style/computed_style.h"
16 #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h"
17 
18 namespace blink {
19 
20 // Returns true if items builder is used for other than offset mapping.
21 template <typename OffsetMappingBuilder>
NeedsBoxInfo()22 bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::NeedsBoxInfo() {
23   return !std::is_same<NGOffsetMappingBuilder, OffsetMappingBuilder>::value;
24 }
25 
26 template <typename OffsetMappingBuilder>
27 NGInlineItemsBuilderTemplate<
~NGInlineItemsBuilderTemplate()28     OffsetMappingBuilder>::~NGInlineItemsBuilderTemplate() {
29   DCHECK_EQ(0u, bidi_context_.size());
30   DCHECK_EQ(text_.length(), items_->IsEmpty() ? 0 : items_->back().EndOffset());
31 }
32 
33 template <typename OffsetMappingBuilder>
ToString()34 String NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ToString() {
35   return text_.ToString();
36 }
37 
38 namespace {
39 // The spec turned into a discussion that may change. Put this logic on hold
40 // until CSSWG resolves the issue.
41 // https://github.com/w3c/csswg-drafts/issues/337
42 #define SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH 0
43 
44 #if SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH
45 // Determine "Ambiguous" East Asian Width is Wide or Narrow.
46 // Unicode East Asian Width
47 // http://unicode.org/reports/tr11/
IsAmbiguosEastAsianWidthWide(const ComputedStyle * style)48 bool IsAmbiguosEastAsianWidthWide(const ComputedStyle* style) {
49   UScriptCode script = style->GetFontDescription().GetScript();
50   return script == USCRIPT_KATAKANA_OR_HIRAGANA ||
51          script == USCRIPT_SIMPLIFIED_HAN || script == USCRIPT_TRADITIONAL_HAN;
52 }
53 
54 // Determine if a character has "Wide" East Asian Width.
IsEastAsianWidthWide(UChar32 c,const ComputedStyle * style)55 bool IsEastAsianWidthWide(UChar32 c, const ComputedStyle* style) {
56   UEastAsianWidth eaw = static_cast<UEastAsianWidth>(
57       u_getIntPropertyValue(c, UCHAR_EAST_ASIAN_WIDTH));
58   return eaw == U_EA_WIDE || eaw == U_EA_FULLWIDTH || eaw == U_EA_HALFWIDTH ||
59          (eaw == U_EA_AMBIGUOUS && style &&
60           IsAmbiguosEastAsianWidthWide(style));
61 }
62 #endif
63 
64 // Determine whether a newline should be removed or not.
65 // CSS Text, Segment Break Transformation Rules
66 // https://drafts.csswg.org/css-text-3/#line-break-transform
ShouldRemoveNewlineSlow(const StringBuilder & before,unsigned space_index,const ComputedStyle * before_style,const StringView & after,const ComputedStyle * after_style)67 bool ShouldRemoveNewlineSlow(const StringBuilder& before,
68                              unsigned space_index,
69                              const ComputedStyle* before_style,
70                              const StringView& after,
71                              const ComputedStyle* after_style) {
72   // Remove if either before/after the newline is zeroWidthSpaceCharacter.
73   UChar32 last = 0;
74   DCHECK(space_index == before.length() ||
75          (space_index < before.length() && before[space_index] == ' '));
76   if (space_index) {
77     last = before[space_index - 1];
78     if (last == kZeroWidthSpaceCharacter)
79       return true;
80   }
81   UChar32 next = 0;
82   if (!after.IsEmpty()) {
83     next = after[0];
84     if (next == kZeroWidthSpaceCharacter)
85       return true;
86   }
87 
88 #if SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH
89   // Logic below this point requires both before and after be 16 bits.
90   if (before.Is8Bit() || after.Is8Bit())
91     return false;
92 
93   // Remove if East Asian Widths of both before/after the newline are Wide, and
94   // neither side is Hangul.
95   // TODO(layout-dev): Don't remove if any side is Emoji.
96   if (U16_IS_TRAIL(last) && space_index >= 2) {
97     UChar last_last = before[space_index - 2];
98     if (U16_IS_LEAD(last_last))
99       last = U16_GET_SUPPLEMENTARY(last_last, last);
100   }
101   if (!Character::IsHangul(last) && IsEastAsianWidthWide(last, before_style)) {
102     if (U16_IS_LEAD(next) && after.length() > 1) {
103       UChar next_next = after[1];
104       if (U16_IS_TRAIL(next_next))
105         next = U16_GET_SUPPLEMENTARY(next, next_next);
106     }
107     if (!Character::IsHangul(next) && IsEastAsianWidthWide(next, after_style))
108       return true;
109   }
110 #endif
111 
112   return false;
113 }
114 
ShouldRemoveNewline(const StringBuilder & before,unsigned space_index,const ComputedStyle * before_style,const StringView & after,const ComputedStyle * after_style)115 bool ShouldRemoveNewline(const StringBuilder& before,
116                          unsigned space_index,
117                          const ComputedStyle* before_style,
118                          const StringView& after,
119                          const ComputedStyle* after_style) {
120   // All characters before/after removable newline are 16 bits.
121   return (!before.Is8Bit() || !after.Is8Bit()) &&
122          ShouldRemoveNewlineSlow(before, space_index, before_style, after,
123                                  after_style);
124 }
125 
AppendItem(Vector<NGInlineItem> * items,NGInlineItem::NGInlineItemType type,unsigned start,unsigned end,LayoutObject * layout_object)126 inline NGInlineItem& AppendItem(Vector<NGInlineItem>* items,
127                                 NGInlineItem::NGInlineItemType type,
128                                 unsigned start,
129                                 unsigned end,
130                                 LayoutObject* layout_object) {
131   return items->emplace_back(type, start, end, layout_object);
132 }
133 
ShouldIgnore(UChar c)134 inline bool ShouldIgnore(UChar c) {
135   // Ignore carriage return and form feed.
136   // https://drafts.csswg.org/css-text-3/#white-space-processing
137   // https://github.com/w3c/csswg-drafts/issues/855
138   //
139   // Unicode Default_Ignorable is not included because we need some of them
140   // in the line breaker (e.g., SOFT HYPHEN.) HarfBuzz ignores them while
141   // shaping.
142   return c == kCarriageReturnCharacter || c == kFormFeedCharacter;
143 }
144 
145 // Characters needing a separate control item than other text items.
146 // It makes the line breaker easier to handle.
IsControlItemCharacter(UChar c)147 inline bool IsControlItemCharacter(UChar c) {
148   return c == kNewlineCharacter || c == kTabulationCharacter ||
149          // Make ZWNJ a control character so that it can prevent kerning.
150          c == kZeroWidthNonJoinerCharacter ||
151          // Include ignorable character here to avoids shaping/rendering
152          // these glyphs, and to help the line breaker to ignore them.
153          ShouldIgnore(c);
154 }
155 
156 // Find the end of the collapsible spaces.
157 // Returns whether this space run contains a newline or not, because it changes
158 // the collapsing behavior.
MoveToEndOfCollapsibleSpaces(const StringView & string,unsigned * offset,UChar * c)159 inline bool MoveToEndOfCollapsibleSpaces(const StringView& string,
160                                          unsigned* offset,
161                                          UChar* c) {
162   DCHECK_EQ(*c, string[*offset]);
163   DCHECK(Character::IsCollapsibleSpace(*c));
164   bool space_run_has_newline = *c == kNewlineCharacter;
165   for ((*offset)++; *offset < string.length(); (*offset)++) {
166     *c = string[*offset];
167     space_run_has_newline |= *c == kNewlineCharacter;
168     if (!Character::IsCollapsibleSpace(*c))
169       break;
170   }
171   return space_run_has_newline;
172 }
173 
174 // Find the last item to compute collapsing with. Opaque items such as
175 // open/close or bidi controls are ignored.
176 // Returns nullptr if there were no previous items.
LastItemToCollapseWith(Vector<NGInlineItem> * items)177 NGInlineItem* LastItemToCollapseWith(Vector<NGInlineItem>* items) {
178   for (auto it = items->rbegin(); it != items->rend(); it++) {
179     NGInlineItem& item = *it;
180     if (item.EndCollapseType() != NGInlineItem::kOpaqueToCollapsing)
181       return &item;
182   }
183   return nullptr;
184 }
185 
186 }  // anonymous namespace
187 
188 template <typename OffsetMappingBuilder>
BoxInfo(unsigned item_index,const NGInlineItem & item)189 NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::BoxInfo::BoxInfo(
190     unsigned item_index,
191     const NGInlineItem& item)
192     : item_index(item_index),
193       should_create_box_fragment(item.ShouldCreateBoxFragment()),
194       may_have_margin_(item.Style()->MayHaveMargin()),
195       text_metrics(item.Style()->GetFontHeight()) {
196   DCHECK(item.Style());
197 }
198 
199 // True if this inline box should create a box fragment when it has |child|.
200 template <typename OffsetMappingBuilder>
201 bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::BoxInfo::
ShouldCreateBoxFragmentForChild(const BoxInfo & child) const202     ShouldCreateBoxFragmentForChild(const BoxInfo& child) const {
203   // When a child inline box has margins, the parent has different width/height
204   // from the union of children.
205   if (child.may_have_margin_)
206     return true;
207 
208   // Returns true when parent and child boxes have different font metrics, since
209   // they may have different heights and/or locations in block direction.
210   if (text_metrics != child.text_metrics)
211     return true;
212 
213   return false;
214 }
215 
216 template <typename OffsetMappingBuilder>
217 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::BoxInfo::
SetShouldCreateBoxFragment(Vector<NGInlineItem> * items)218     SetShouldCreateBoxFragment(Vector<NGInlineItem>* items) {
219   DCHECK(!should_create_box_fragment);
220   should_create_box_fragment = true;
221   (*items)[item_index].SetShouldCreateBoxFragment();
222 }
223 
224 // Append a string as a text item.
225 template <typename OffsetMappingBuilder>
AppendTextItem(const StringView string,LayoutText * layout_object)226 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextItem(
227     const StringView string,
228     LayoutText* layout_object) {
229   DCHECK(layout_object);
230   AppendTextItem(NGInlineItem::kText, string, layout_object);
231 }
232 
233 template <typename OffsetMappingBuilder>
234 NGInlineItem&
AppendTextItem(NGInlineItem::NGInlineItemType type,const StringView string,LayoutText * layout_object)235 NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextItem(
236     NGInlineItem::NGInlineItemType type,
237     const StringView string,
238     LayoutText* layout_object) {
239   DCHECK(layout_object);
240   unsigned start_offset = text_.length();
241   text_.Append(string);
242   mapping_builder_.AppendIdentityMapping(string.length());
243   NGInlineItem& item =
244       AppendItem(items_, type, start_offset, text_.length(), layout_object);
245   DCHECK(!item.IsEmptyItem());
246   // text item is not empty.
247   is_empty_inline_ = false;
248   is_block_level_ = false;
249   return item;
250 }
251 
252 // Empty text items are not needed for the layout purposes, but all LayoutObject
253 // must be captured in NGInlineItemsData to maintain states of LayoutObject in
254 // this inline formatting context.
255 template <typename OffsetMappingBuilder>
AppendEmptyTextItem(LayoutText * layout_object)256 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendEmptyTextItem(
257     LayoutText* layout_object) {
258   DCHECK(layout_object);
259   unsigned offset = text_.length();
260   NGInlineItem& item =
261       AppendItem(items_, NGInlineItem::kText, offset, offset, layout_object);
262   item.SetEndCollapseType(NGInlineItem::kOpaqueToCollapsing);
263   item.SetIsEmptyItem(true);
264   item.SetIsBlockLevel(true);
265 }
266 
267 // Same as AppendBreakOpportunity, but mark the item as IsGenerated().
268 template <typename OffsetMappingBuilder>
269 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::
AppendGeneratedBreakOpportunity(LayoutObject * layout_object)270     AppendGeneratedBreakOpportunity(LayoutObject* layout_object) {
271   DCHECK(layout_object);
272   typename OffsetMappingBuilder::SourceNodeScope scope(&mapping_builder_,
273                                                        nullptr);
274   NGInlineItem& item = AppendBreakOpportunity(layout_object);
275   item.SetIsGeneratedForLineBreak();
276   item.SetEndCollapseType(NGInlineItem::kOpaqueToCollapsing);
277 }
278 
279 template <typename OffsetMappingBuilder>
AppendTextReusing(const NGInlineNodeData & original_data,LayoutText * layout_text)280 bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing(
281     const NGInlineNodeData& original_data,
282     LayoutText* layout_text) {
283   DCHECK(layout_text);
284   const base::span<NGInlineItem>& items = layout_text->InlineItems();
285   const NGInlineItem& old_item0 = items.front();
286   if (!old_item0.Length())
287     return false;
288 
289   const String& original_string = original_data.text_content;
290 
291   // Don't reuse existing items if they might be affected by whitespace
292   // collapsing.
293   // TODO(layout-dev): This could likely be optimized further.
294   // TODO(layout-dev): Handle cases where the old items are not consecutive.
295   const ComputedStyle& new_style = layout_text->StyleRef();
296   bool collapse_spaces = new_style.CollapseWhiteSpace();
297   bool preserve_newlines = new_style.PreserveNewline();
298   if (NGInlineItem* last_item = LastItemToCollapseWith(items_)) {
299     if (collapse_spaces) {
300       switch (last_item->EndCollapseType()) {
301         case NGInlineItem::kCollapsible:
302           switch (original_string[old_item0.StartOffset()]) {
303             case kSpaceCharacter:
304               // If the original string starts with a collapsible space, it may
305               // be collapsed.
306               return false;
307             case kNewlineCharacter:
308               // Collapsible spaces immediately before a preserved newline
309               // should be removed to be consistent with
310               // AppendForcedBreakCollapseWhitespace.
311               if (preserve_newlines)
312                 return false;
313           }
314           // If the last item ended with a collapsible space run with segment
315           // breaks, we need to run the full algorithm to apply segment break
316           // rules. This may result in removal of the space in the last item.
317           if (last_item->IsEndCollapsibleNewline()) {
318             const StringView old_item0_view(
319                 original_string, old_item0.StartOffset(), old_item0.Length());
320             if (ShouldRemoveNewline(text_, last_item->EndOffset() - 1,
321                                     last_item->Style(), old_item0_view,
322                                     &new_style)) {
323               return false;
324             }
325           }
326           break;
327         case NGInlineItem::kNotCollapsible: {
328           const String& source_text = layout_text->GetText();
329           if (source_text.length() &&
330               Character::IsCollapsibleSpace(source_text[0])) {
331             // If the start of the original string was collapsed, it may be
332             // restored.
333             if (original_string[old_item0.StartOffset()] != kSpaceCharacter)
334               return false;
335             // If the start of the original string was not collapsed, and the
336             // collapsible space run contains newline, the newline may be
337             // removed.
338             unsigned offset = 0;
339             UChar c = source_text[0];
340             bool contains_newline =
341                 MoveToEndOfCollapsibleSpaces(source_text, &offset, &c);
342             if (contains_newline &&
343                 ShouldRemoveNewline(text_, text_.length(), last_item->Style(),
344                                     StringView(source_text, offset),
345                                     &new_style)) {
346               return false;
347             }
348           }
349           break;
350         }
351         case NGInlineItem::kCollapsed:
352           RestoreTrailingCollapsibleSpace(last_item);
353           return false;
354         case NGInlineItem::kOpaqueToCollapsing:
355           NOTREACHED();
356           break;
357       }
358     } else if (last_item->EndCollapseType() == NGInlineItem::kCollapsed) {
359       RestoreTrailingCollapsibleSpace(last_item);
360       return false;
361     }
362 
363     // On nowrap -> wrap boundary, a break opporunity may be inserted.
364     DCHECK(last_item->Style());
365     if (!last_item->Style()->AutoWrap() && new_style.AutoWrap())
366       return false;
367 
368   } else if (collapse_spaces) {
369     // If the original string starts with a collapsible space, it may be
370     // collapsed because it is now a leading collapsible space.
371     if (original_string[old_item0.StartOffset()] == kSpaceCharacter)
372       return false;
373   }
374 
375   if (preserve_newlines) {
376     // We exit and then re-enter all bidi contexts around a forced break. So, We
377     // must go through the full pipeline to ensure that we exit and enter the
378     // correct bidi contexts the re-layout.
379     if (bidi_context_.size() || layout_text->HasBidiControlInlineItems()) {
380       if (layout_text->GetText().Contains(kNewlineCharacter))
381         return false;
382     }
383   }
384 
385   if (UNLIKELY(old_item0.StartOffset() > 0 &&
386                ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces(
387                    layout_text->GetText(), new_style))) {
388     // e.g. <p>abc xyz</p> => <p> xyz</p> where "abc" and " xyz" are different
389     // Text node. |text_| is " \u200Bxyz".
390     return false;
391   }
392 
393   for (const NGInlineItem& item : items) {
394     // Collapsed space item at the start will not be restored, and that not
395     // needed to add.
396     if (!text_.length() && !item.Length() && collapse_spaces)
397       continue;
398 
399     unsigned start = text_.length();
400     text_.Append(original_string, item.StartOffset(), item.Length());
401 
402     // If the item's position within the container remains unchanged the item
403     // itself may be reused.
404     if (item.StartOffset() == start) {
405       items_->push_back(item);
406       is_empty_inline_ &= item.IsEmptyItem();
407       is_block_level_ &= item.IsBlockLevel();
408       continue;
409     }
410 
411     // If the position has shifted the item and the shape result needs to be
412     // adjusted to reflect the new start and end offsets.
413     unsigned end = start + item.Length();
414     scoped_refptr<ShapeResult> adjusted_shape_result;
415     if (item.TextShapeResult()) {
416       DCHECK_EQ(item.Type(), NGInlineItem::kText);
417       adjusted_shape_result = item.TextShapeResult()->CopyAdjustedOffset(start);
418       DCHECK(adjusted_shape_result);
419     } else {
420       // The following should be true, but some unit tests fail.
421       // DCHECK_EQ(item->Type(), NGInlineItem::kControl);
422     }
423     NGInlineItem adjusted_item(item, start, end,
424                                std::move(adjusted_shape_result));
425 
426 #if DCHECK_IS_ON()
427     DCHECK_EQ(start, adjusted_item.StartOffset());
428     DCHECK_EQ(end, adjusted_item.EndOffset());
429     if (adjusted_item.TextShapeResult()) {
430       DCHECK_EQ(start, adjusted_item.TextShapeResult()->StartIndex());
431       DCHECK_EQ(end, adjusted_item.TextShapeResult()->EndIndex());
432     }
433     DCHECK_EQ(item.IsEmptyItem(), adjusted_item.IsEmptyItem());
434 #endif
435 
436     items_->push_back(adjusted_item);
437     is_empty_inline_ &= adjusted_item.IsEmptyItem();
438     is_block_level_ &= adjusted_item.IsBlockLevel();
439   }
440   return true;
441 }
442 
443 template <>
AppendTextReusing(const NGInlineNodeData &,LayoutText *)444 bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::AppendTextReusing(
445     const NGInlineNodeData&,
446     LayoutText*) {
447   NOTREACHED();
448   return false;
449 }
450 
451 template <typename OffsetMappingBuilder>
AppendText(LayoutText * layout_text,const NGInlineNodeData * previous_data)452 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendText(
453     LayoutText* layout_text,
454     const NGInlineNodeData* previous_data) {
455   // If the LayoutText element hasn't changed, reuse the existing items.
456   if (previous_data && layout_text->HasValidInlineItems()) {
457     if (AppendTextReusing(*previous_data, layout_text)) {
458       return;
459     }
460   }
461 
462   // If not create a new item as needed.
463   if (UNLIKELY(layout_text->IsWordBreak())) {
464     typename OffsetMappingBuilder::SourceNodeScope scope(&mapping_builder_,
465                                                          layout_text);
466     AppendBreakOpportunity(layout_text);
467     return;
468   }
469 
470   AppendText(layout_text->GetText(), layout_text);
471 }
472 
473 template <typename OffsetMappingBuilder>
AppendText(const String & string,LayoutText * layout_object)474 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendText(
475     const String& string,
476     LayoutText* layout_object) {
477   DCHECK(layout_object);
478 
479   if (string.IsEmpty()) {
480     AppendEmptyTextItem(layout_object);
481     return;
482   }
483   text_.ReserveCapacity(string.length());
484 
485   typename OffsetMappingBuilder::SourceNodeScope scope(&mapping_builder_,
486                                                        layout_object);
487 
488   const ComputedStyle& style = layout_object->StyleRef();
489   EWhiteSpace whitespace = style.WhiteSpace();
490   bool is_svg_text = layout_object && layout_object->IsSVGInlineText();
491 
492   RestoreTrailingCollapsibleSpaceIfRemoved();
493 
494   if (!ComputedStyle::CollapseWhiteSpace(whitespace))
495     AppendPreserveWhitespace(string, &style, layout_object);
496   else if (ComputedStyle::PreserveNewline(whitespace) && !is_svg_text)
497     AppendPreserveNewline(string, &style, layout_object);
498   else
499     AppendCollapseWhitespace(string, &style, layout_object);
500 }
501 
502 template <typename OffsetMappingBuilder>
503 void NGInlineItemsBuilderTemplate<
AppendCollapseWhitespace(const StringView string,const ComputedStyle * style,LayoutText * layout_object)504     OffsetMappingBuilder>::AppendCollapseWhitespace(const StringView string,
505                                                     const ComputedStyle* style,
506                                                     LayoutText* layout_object) {
507   DCHECK(!string.IsEmpty());
508 
509   // This algorithm segments the input string at the collapsible space, and
510   // process collapsible space run and non-space run alternately.
511 
512   // The first run, regardless it is a collapsible space run or not, is special
513   // that it can interact with the last item. Depends on the end of the last
514   // item, it may either change collapsing behavior to collapse the leading
515   // spaces of this item entirely, or remove the trailing spaces of the last
516   // item.
517 
518   // Due to this difference, this algorithm process the first run first, then
519   // loop through the rest of runs.
520 
521   unsigned start_offset;
522   NGInlineItem::NGCollapseType end_collapse = NGInlineItem::kNotCollapsible;
523   unsigned i = 0;
524   UChar c = string[i];
525   bool space_run_has_newline = false;
526   if (Character::IsCollapsibleSpace(c)) {
527     // Find the end of the collapsible space run.
528     space_run_has_newline = MoveToEndOfCollapsibleSpaces(string, &i, &c);
529 
530     // LayoutBR does not set preserve_newline, but should be preserved.
531     if (UNLIKELY(space_run_has_newline && string.length() == 1 &&
532                  layout_object && layout_object->IsBR())) {
533       AppendForcedBreakCollapseWhitespace(layout_object);
534       return;
535     }
536 
537     // Check the last item this space run may be collapsed with.
538     bool insert_space;
539     if (NGInlineItem* item = LastItemToCollapseWith(items_)) {
540       if (item->EndCollapseType() == NGInlineItem::kNotCollapsible) {
541         // The last item does not end with a collapsible space.
542         // Insert a space to represent this space run.
543         insert_space = true;
544       } else {
545         // The last item ends with a collapsible space this run should collapse
546         // to. Collapse the entire space run in this item.
547         DCHECK(item->EndCollapseType() == NGInlineItem::kCollapsible);
548         insert_space = false;
549 
550         // If the space run either in this item or in the last item contains a
551         // newline, apply segment break rules. This may result in removal of
552         // the space in the last item.
553         if ((space_run_has_newline || item->IsEndCollapsibleNewline()) &&
554             item->Type() == NGInlineItem::kText &&
555             ShouldRemoveNewline(text_, item->EndOffset() - 1, item->Style(),
556                                 StringView(string, i), style)) {
557           RemoveTrailingCollapsibleSpace(item);
558           space_run_has_newline = false;
559         } else if (!item->Style()->AutoWrap() && style->AutoWrap()) {
560           // Otherwise, remove the space run entirely, collapsing to the space
561           // in the last item.
562 
563           // There is a special case to generate a break opportunity though.
564           // Spec-wise, collapsed spaces are "zero advance width, invisible,
565           // but retains its soft wrap opportunity".
566           // https://drafts.csswg.org/css-text-3/#collapse
567           // In most cases, this is not needed and that collapsed spaces are
568           // removed entirely. However, when the first collapsible space is
569           // 'nowrap', and the following collapsed space is 'wrap', the
570           // collapsed space needs to create a break opportunity.
571           // Note that we don't need to generate a break opportunity right
572           // after a forced break.
573           if (item->Type() != NGInlineItem::kControl ||
574               text_[item->StartOffset()] != kNewlineCharacter) {
575             AppendGeneratedBreakOpportunity(layout_object);
576           }
577         }
578       }
579     } else {
580       // This space is at the beginning of the paragraph. Remove leading spaces
581       // as CSS requires.
582       insert_space = false;
583     }
584 
585     // If this space run contains a newline, apply segment break rules.
586     if (space_run_has_newline &&
587         ShouldRemoveNewline(text_, text_.length(), style, StringView(string, i),
588                             style)) {
589       insert_space = space_run_has_newline = false;
590     }
591 
592     // Done computing the interaction with the last item. Start appending.
593     start_offset = text_.length();
594 
595     DCHECK(i);
596     unsigned collapsed_length = i;
597     if (insert_space) {
598       text_.Append(kSpaceCharacter);
599       mapping_builder_.AppendIdentityMapping(1);
600       collapsed_length--;
601     }
602     if (collapsed_length)
603       mapping_builder_.AppendCollapsedMapping(collapsed_length);
604 
605     // If this space run is at the end of this item, keep whether the
606     // collapsible space run has a newline or not in the item.
607     if (i == string.length()) {
608       end_collapse = NGInlineItem::kCollapsible;
609     }
610   } else {
611     // If the last item ended with a collapsible space run with segment breaks,
612     // apply segment break rules. This may result in removal of the space in the
613     // last item.
614     if (NGInlineItem* item = LastItemToCollapseWith(items_)) {
615       if (item->EndCollapseType() == NGInlineItem::kCollapsible &&
616           item->IsEndCollapsibleNewline() &&
617           ShouldRemoveNewline(text_, item->EndOffset() - 1, item->Style(),
618                               string, style)) {
619         RemoveTrailingCollapsibleSpace(item);
620       }
621     }
622 
623     start_offset = text_.length();
624   }
625 
626   // The first run is done. Loop through the rest of runs.
627   if (i < string.length()) {
628     while (true) {
629       // Append the non-space text until we find a collapsible space.
630       // |string[i]| is guaranteed not to be a space.
631       DCHECK(!Character::IsCollapsibleSpace(string[i]));
632       unsigned start_of_non_space = i;
633       for (i++; i < string.length(); i++) {
634         c = string[i];
635         if (Character::IsCollapsibleSpace(c))
636           break;
637       }
638       text_.Append(string, start_of_non_space, i - start_of_non_space);
639       mapping_builder_.AppendIdentityMapping(i - start_of_non_space);
640 
641       if (i == string.length()) {
642         end_collapse = NGInlineItem::kNotCollapsible;
643         break;
644       }
645 
646       // Process a collapsible space run. First, find the end of the run.
647       DCHECK_EQ(c, string[i]);
648       DCHECK(Character::IsCollapsibleSpace(c));
649       unsigned start_of_spaces = i;
650       space_run_has_newline = MoveToEndOfCollapsibleSpaces(string, &i, &c);
651 
652       // Because leading spaces are handled before this loop, no need to check
653       // cross-item collapsing.
654       DCHECK(start_of_spaces);
655 
656       // If this space run contains a newline, apply segment break rules.
657       bool remove_newline = space_run_has_newline &&
658                             ShouldRemoveNewline(text_, text_.length(), style,
659                                                 StringView(string, i), style);
660       if (UNLIKELY(remove_newline)) {
661         // |kNotCollapsible| because the newline is removed, not collapsed.
662         end_collapse = NGInlineItem::kNotCollapsible;
663         space_run_has_newline = false;
664       } else {
665         // If the segment break rules did not remove the run, append a space.
666         text_.Append(kSpaceCharacter);
667         mapping_builder_.AppendIdentityMapping(1);
668         start_of_spaces++;
669         end_collapse = NGInlineItem::kCollapsible;
670       }
671 
672       if (i != start_of_spaces)
673         mapping_builder_.AppendCollapsedMapping(i - start_of_spaces);
674 
675       // If this space run is at the end of this item, keep whether the
676       // collapsible space run has a newline or not in the item.
677       if (i == string.length()) {
678         break;
679       }
680     }
681   }
682 
683   DCHECK_GE(text_.length(), start_offset);
684   if (UNLIKELY(text_.length() == start_offset)) {
685     AppendEmptyTextItem(layout_object);
686     return;
687   }
688 
689   NGInlineItem& item = AppendItem(items_, NGInlineItem::kText, start_offset,
690                                   text_.length(), layout_object);
691   item.SetEndCollapseType(end_collapse, space_run_has_newline);
692   DCHECK(!item.IsEmptyItem());
693   // text item is not empty.
694   is_empty_inline_ = false;
695   is_block_level_ = false;
696 }
697 
698 template <typename OffsetMappingBuilder>
699 bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::
ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces(const String & string,const ComputedStyle & style,unsigned index) const700     ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces(
701         const String& string,
702         const ComputedStyle& style,
703         unsigned index) const {
704   DCHECK_LE(index, string.length());
705   // Check if we are at a preserved space character and auto-wrap is enabled.
706   if (style.CollapseWhiteSpace() || !style.AutoWrap() || !string.length() ||
707       index >= string.length() || string[index] != kSpaceCharacter)
708     return false;
709 
710   // Preserved leading spaces must be at the beginning of the first line or just
711   // after a forced break.
712   if (index)
713     return string[index - 1] == kNewlineCharacter;
714   return text_.IsEmpty() || text_[text_.length() - 1] == kNewlineCharacter;
715 }
716 
717 template <typename OffsetMappingBuilder>
718 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::
InsertBreakOpportunityAfterLeadingPreservedSpaces(const String & string,const ComputedStyle & style,LayoutText * layout_object,unsigned * start)719     InsertBreakOpportunityAfterLeadingPreservedSpaces(
720         const String& string,
721         const ComputedStyle& style,
722         LayoutText* layout_object,
723         unsigned* start) {
724   DCHECK(start);
725   if (UNLIKELY(ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces(
726           string, style, *start))) {
727     wtf_size_t end = *start;
728     do {
729       ++end;
730     } while (end < string.length() && string[end] == kSpaceCharacter);
731     AppendTextItem(StringView(string, *start, end - *start), layout_object);
732     AppendGeneratedBreakOpportunity(layout_object);
733     *start = end;
734   }
735 }
736 
737 // TODO(yosin): We should remove |style| and |string| parameter because of
738 // except for testing, we can get them from |LayoutText|.
739 // Even when without whitespace collapsing, control characters (newlines and
740 // tabs) are in their own control items to make the line breaker not special.
741 template <typename OffsetMappingBuilder>
742 void NGInlineItemsBuilderTemplate<
AppendPreserveWhitespace(const String & string,const ComputedStyle * style,LayoutText * layout_object)743     OffsetMappingBuilder>::AppendPreserveWhitespace(const String& string,
744                                                     const ComputedStyle* style,
745                                                     LayoutText* layout_object) {
746   DCHECK(style);
747 
748   // A soft wrap opportunity exists at the end of the sequence of preserved
749   // spaces. https://drafts.csswg.org/css-text-3/#white-space-phase-1
750   // Due to our optimization to give opportunities before spaces, the
751   // opportunity after leading preserved spaces needs a special code in the line
752   // breaker. Generate an opportunity to make it easy.
753   unsigned start = 0;
754   InsertBreakOpportunityAfterLeadingPreservedSpaces(string, *style,
755                                                     layout_object, &start);
756   for (; start < string.length();) {
757     UChar c = string[start];
758     if (IsControlItemCharacter(c)) {
759       if (c == kNewlineCharacter) {
760         AppendForcedBreak(layout_object);
761         start++;
762         // A forced break is not a collapsible space, but following collapsible
763         // spaces are leading spaces and they need a special code in the line
764         // breaker. Generate an opportunity to make it easy.
765         InsertBreakOpportunityAfterLeadingPreservedSpaces(
766             string, *style, layout_object, &start);
767         continue;
768       }
769       if (c == kTabulationCharacter) {
770         wtf_size_t end = string.Find(
771             [](UChar c) { return c != kTabulationCharacter; }, start + 1);
772         if (end == kNotFound)
773           end = string.length();
774         NGInlineItem& item = AppendTextItem(
775             NGInlineItem::kControl, StringView(string, start, end - start),
776             layout_object);
777         item.SetTextType(NGTextType::kFlowControl);
778         start = end;
779         continue;
780       }
781       // ZWNJ splits item, but it should be text.
782       if (c != kZeroWidthNonJoinerCharacter) {
783         NGInlineItem& item = Append(NGInlineItem::kControl, c, layout_object);
784         item.SetTextType(NGTextType::kFlowControl);
785         start++;
786         continue;
787       }
788     }
789 
790     wtf_size_t end = string.Find(IsControlItemCharacter, start + 1);
791     if (end == kNotFound)
792       end = string.length();
793     AppendTextItem(StringView(string, start, end - start), layout_object);
794     start = end;
795   }
796 }
797 
798 template <typename OffsetMappingBuilder>
AppendPreserveNewline(const String & string,const ComputedStyle * style,LayoutText * layout_object)799 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendPreserveNewline(
800     const String& string,
801     const ComputedStyle* style,
802     LayoutText* layout_object) {
803   for (unsigned start = 0; start < string.length();) {
804     if (string[start] == kNewlineCharacter) {
805       AppendForcedBreakCollapseWhitespace(layout_object);
806       start++;
807       continue;
808     }
809 
810     wtf_size_t end = string.find(kNewlineCharacter, start + 1);
811     if (end == kNotFound)
812       end = string.length();
813     DCHECK_GE(end, start);
814     AppendCollapseWhitespace(StringView(string, start, end - start), style,
815                              layout_object);
816     start = end;
817   }
818 }
819 
820 template <typename OffsetMappingBuilder>
AppendForcedBreak(LayoutObject * layout_object)821 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendForcedBreak(
822     LayoutObject* layout_object) {
823   DCHECK(layout_object);
824   // At the forced break, add bidi controls to pop all contexts.
825   // https://drafts.csswg.org/css-writing-modes-3/#bidi-embedding-breaks
826   if (!bidi_context_.IsEmpty()) {
827     typename OffsetMappingBuilder::SourceNodeScope scope(&mapping_builder_,
828                                                          nullptr);
829     // These bidi controls need to be associated with the |layout_object| so
830     // that items from a LayoutObject are consecutive.
831     for (auto it = bidi_context_.rbegin(); it != bidi_context_.rend(); ++it) {
832       AppendOpaque(NGInlineItem::kBidiControl, it->exit, layout_object);
833     }
834   }
835 
836   NGInlineItem& item =
837       Append(NGInlineItem::kControl, kNewlineCharacter, layout_object);
838   item.SetTextType(NGTextType::kForcedLineBreak);
839 
840   // A forced break is not a collapsible space, but following collapsible spaces
841   // are leading spaces and that they should be collapsed.
842   // Pretend that this item ends with a collapsible space, so that following
843   // collapsible spaces can be collapsed.
844   item.SetEndCollapseType(NGInlineItem::kCollapsible, false);
845 
846   // Then re-add bidi controls to restore the bidi context.
847   if (!bidi_context_.IsEmpty()) {
848     typename OffsetMappingBuilder::SourceNodeScope scope(&mapping_builder_,
849                                                          nullptr);
850     for (const auto& bidi : bidi_context_) {
851       AppendOpaque(NGInlineItem::kBidiControl, bidi.enter, layout_object);
852     }
853   }
854 }
855 
856 template <typename OffsetMappingBuilder>
857 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::
AppendForcedBreakCollapseWhitespace(LayoutObject * layout_object)858     AppendForcedBreakCollapseWhitespace(LayoutObject* layout_object) {
859   // Remove collapsible spaces immediately before a preserved newline.
860   RemoveTrailingCollapsibleSpaceIfExists();
861 
862   AppendForcedBreak(layout_object);
863 }
864 
865 template <typename OffsetMappingBuilder>
866 NGInlineItem&
AppendBreakOpportunity(LayoutObject * layout_object)867 NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendBreakOpportunity(
868     LayoutObject* layout_object) {
869   DCHECK(layout_object);
870   NGInlineItem& item = AppendOpaque(NGInlineItem::kControl,
871                                     kZeroWidthSpaceCharacter, layout_object);
872   item.SetTextType(NGTextType::kFlowControl);
873   return item;
874 }
875 
876 template <typename OffsetMappingBuilder>
Append(NGInlineItem::NGInlineItemType type,UChar character,LayoutObject * layout_object)877 NGInlineItem& NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::Append(
878     NGInlineItem::NGInlineItemType type,
879     UChar character,
880     LayoutObject* layout_object) {
881   DCHECK_NE(character, kSpaceCharacter);
882 
883   text_.Append(character);
884   mapping_builder_.AppendIdentityMapping(1);
885   unsigned end_offset = text_.length();
886   NGInlineItem& item =
887       AppendItem(items_, type, end_offset - 1, end_offset, layout_object);
888   is_empty_inline_ &= item.IsEmptyItem();
889   is_block_level_ &= item.IsBlockLevel();
890   return item;
891 }
892 
893 template <typename OffsetMappingBuilder>
AppendAtomicInline(LayoutObject * layout_object)894 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendAtomicInline(
895     LayoutObject* layout_object) {
896   DCHECK(layout_object);
897   typename OffsetMappingBuilder::SourceNodeScope scope(&mapping_builder_,
898                                                        layout_object);
899   RestoreTrailingCollapsibleSpaceIfRemoved();
900   Append(NGInlineItem::kAtomicInline, kObjectReplacementCharacter,
901          layout_object);
902   has_ruby_ = has_ruby_ || layout_object->IsRubyRun();
903 
904   // When this atomic inline is inside of an inline box, the height of the
905   // inline box can be different from the height of the atomic inline. Ensure
906   // the inline box creates a box fragment so that its height is available in
907   // the fragment tree.
908   if (!boxes_.IsEmpty()) {
909     BoxInfo* current_box = &boxes_.back();
910     if (!current_box->should_create_box_fragment)
911       current_box->SetShouldCreateBoxFragment(items_);
912   }
913 }
914 
915 template <typename OffsetMappingBuilder>
AppendFloating(LayoutObject * layout_object)916 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendFloating(
917     LayoutObject* layout_object) {
918   AppendOpaque(NGInlineItem::kFloating, kObjectReplacementCharacter,
919                layout_object);
920 }
921 
922 template <typename OffsetMappingBuilder>
923 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::
AppendOutOfFlowPositioned(LayoutObject * layout_object)924     AppendOutOfFlowPositioned(LayoutObject* layout_object) {
925   AppendOpaque(NGInlineItem::kOutOfFlowPositioned, kObjectReplacementCharacter,
926                layout_object);
927 }
928 
929 template <typename OffsetMappingBuilder>
AppendOpaque(NGInlineItem::NGInlineItemType type,UChar character,LayoutObject * layout_object)930 NGInlineItem& NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendOpaque(
931     NGInlineItem::NGInlineItemType type,
932     UChar character,
933     LayoutObject* layout_object) {
934   text_.Append(character);
935   mapping_builder_.AppendIdentityMapping(1);
936   unsigned end_offset = text_.length();
937   NGInlineItem& item =
938       AppendItem(items_, type, end_offset - 1, end_offset, layout_object);
939   item.SetEndCollapseType(NGInlineItem::kOpaqueToCollapsing);
940   is_empty_inline_ &= item.IsEmptyItem();
941   is_block_level_ &= item.IsBlockLevel();
942   return item;
943 }
944 
945 template <typename OffsetMappingBuilder>
AppendOpaque(NGInlineItem::NGInlineItemType type,LayoutObject * layout_object)946 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendOpaque(
947     NGInlineItem::NGInlineItemType type,
948     LayoutObject* layout_object) {
949   unsigned end_offset = text_.length();
950   NGInlineItem& item =
951       AppendItem(items_, type, end_offset, end_offset, layout_object);
952   item.SetEndCollapseType(NGInlineItem::kOpaqueToCollapsing);
953   is_empty_inline_ &= item.IsEmptyItem();
954   is_block_level_ &= item.IsBlockLevel();
955 }
956 
957 // Removes the collapsible space at the end of |text_| if exists.
958 template <typename OffsetMappingBuilder>
959 void NGInlineItemsBuilderTemplate<
RemoveTrailingCollapsibleSpaceIfExists()960     OffsetMappingBuilder>::RemoveTrailingCollapsibleSpaceIfExists() {
961   if (NGInlineItem* item = LastItemToCollapseWith(items_)) {
962     if (item->EndCollapseType() == NGInlineItem::kCollapsible)
963       RemoveTrailingCollapsibleSpace(item);
964   }
965 }
966 
967 // Removes the collapsible space at the end of the specified item.
968 template <typename OffsetMappingBuilder>
969 void NGInlineItemsBuilderTemplate<
RemoveTrailingCollapsibleSpace(NGInlineItem * item)970     OffsetMappingBuilder>::RemoveTrailingCollapsibleSpace(NGInlineItem* item) {
971   DCHECK(item);
972   DCHECK_EQ(item->EndCollapseType(), NGInlineItem::kCollapsible);
973   DCHECK_GT(item->Length(), 0u);
974 
975   // A forced break pretends that it's a collapsible space, see
976   // |AppendForcedBreak()|. It should not be removed.
977   if (item->Type() == NGInlineItem::kControl)
978     return;
979   DCHECK_EQ(item->Type(), NGInlineItem::kText);
980 
981   DCHECK_GT(item->EndOffset(), item->StartOffset());
982   unsigned space_offset = item->EndOffset() - 1;
983   DCHECK_EQ(text_[space_offset], kSpaceCharacter);
984   text_.erase(space_offset);
985   mapping_builder_.CollapseTrailingSpace(space_offset);
986 
987   // Keep the item even if the length became zero. This is not needed for
988   // the layout purposes, but needed to maintain LayoutObject states. See
989   // |AppendEmptyTextItem()|.
990   item->SetEndOffset(item->EndOffset() - 1);
991   item->SetEndCollapseType(NGInlineItem::kCollapsed);
992 
993   // Trailing spaces can be removed across non-character items.
994   // Adjust their offsets if after the removed index.
995   for (item++; item != items_->end(); item++) {
996     item->SetOffset(item->StartOffset() - 1, item->EndOffset() - 1);
997   }
998 }
999 
1000 // Restore removed collapsible space at the end of items.
1001 template <typename OffsetMappingBuilder>
1002 void NGInlineItemsBuilderTemplate<
RestoreTrailingCollapsibleSpaceIfRemoved()1003     OffsetMappingBuilder>::RestoreTrailingCollapsibleSpaceIfRemoved() {
1004   if (NGInlineItem* last_item = LastItemToCollapseWith(items_)) {
1005     if (last_item->EndCollapseType() == NGInlineItem::kCollapsed)
1006       RestoreTrailingCollapsibleSpace(last_item);
1007   }
1008 }
1009 
1010 // Restore removed collapsible space at the end of the specified item.
1011 template <typename OffsetMappingBuilder>
1012 void NGInlineItemsBuilderTemplate<
RestoreTrailingCollapsibleSpace(NGInlineItem * item)1013     OffsetMappingBuilder>::RestoreTrailingCollapsibleSpace(NGInlineItem* item) {
1014   DCHECK(item);
1015   DCHECK(item->EndCollapseType() == NGInlineItem::kCollapsed);
1016 
1017   mapping_builder_.RestoreTrailingCollapsibleSpace(
1018       To<LayoutText>(*item->GetLayoutObject()), item->EndOffset());
1019 
1020   // TODO(kojii): Implement StringBuilder::insert().
1021   if (text_.length() == item->EndOffset()) {
1022     text_.Append(' ');
1023   } else {
1024     String current = text_.ToString();
1025     text_.Clear();
1026     text_.Append(StringView(current, 0, item->EndOffset()));
1027     text_.Append(' ');
1028     text_.Append(StringView(current, item->EndOffset()));
1029   }
1030 
1031   item->SetEndOffset(item->EndOffset() + 1);
1032   item->SetEndCollapseType(NGInlineItem::kCollapsible);
1033 
1034   for (item++; item != items_->end(); item++) {
1035     item->SetOffset(item->StartOffset() + 1, item->EndOffset() + 1);
1036   }
1037 }
1038 
1039 template <typename OffsetMappingBuilder>
EnterBidiContext(LayoutObject * node,UChar enter,UChar exit)1040 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterBidiContext(
1041     LayoutObject* node,
1042     UChar enter,
1043     UChar exit) {
1044   AppendOpaque(NGInlineItem::kBidiControl, enter);
1045   bidi_context_.push_back(BidiContext{node, enter, exit});
1046   has_bidi_controls_ = true;
1047 }
1048 
1049 template <typename OffsetMappingBuilder>
EnterBidiContext(LayoutObject * node,const ComputedStyle * style,UChar ltr_enter,UChar rtl_enter,UChar exit)1050 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterBidiContext(
1051     LayoutObject* node,
1052     const ComputedStyle* style,
1053     UChar ltr_enter,
1054     UChar rtl_enter,
1055     UChar exit) {
1056   EnterBidiContext(node, IsLtr(style->Direction()) ? ltr_enter : rtl_enter,
1057                    exit);
1058 }
1059 
1060 template <typename OffsetMappingBuilder>
EnterBlock(const ComputedStyle * style)1061 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterBlock(
1062     const ComputedStyle* style) {
1063   // Handle bidi-override on the block itself.
1064   if (style->RtlOrdering() == EOrder::kLogical) {
1065     switch (style->GetUnicodeBidi()) {
1066       case UnicodeBidi::kNormal:
1067       case UnicodeBidi::kEmbed:
1068       case UnicodeBidi::kIsolate:
1069         // Isolate and embed values are enforced by default and redundant on the
1070         // block elements.
1071         // Direction is handled as the paragraph level by
1072         // NGBidiParagraph::SetParagraph().
1073         if (style->Direction() == TextDirection::kRtl)
1074           has_bidi_controls_ = true;
1075         break;
1076       case UnicodeBidi::kBidiOverride:
1077       case UnicodeBidi::kIsolateOverride:
1078         EnterBidiContext(nullptr, style, kLeftToRightOverrideCharacter,
1079                          kRightToLeftOverrideCharacter,
1080                          kPopDirectionalFormattingCharacter);
1081         break;
1082       case UnicodeBidi::kPlaintext:
1083         // Plaintext is handled as the paragraph level by
1084         // NGBidiParagraph::SetParagraph().
1085         has_bidi_controls_ = true;
1086         // It's not easy to compute which lines will change with `unicode-bidi:
1087         // plaintext`. Since it is quite uncommon that just disable line cache.
1088         has_unicode_bidi_plain_text_ = true;
1089         break;
1090     }
1091   } else {
1092     DCHECK_EQ(style->RtlOrdering(), EOrder::kVisual);
1093     EnterBidiContext(nullptr, style, kLeftToRightOverrideCharacter,
1094                      kRightToLeftOverrideCharacter,
1095                      kPopDirectionalFormattingCharacter);
1096   }
1097 
1098   if (style->Display() == EDisplay::kListItem &&
1099       style->ListStyleType() != EListStyleType::kNone) {
1100     is_empty_inline_ = false;
1101     is_block_level_ = false;
1102   }
1103 }
1104 
1105 template <typename OffsetMappingBuilder>
EnterInline(LayoutInline * node)1106 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterInline(
1107     LayoutInline* node) {
1108   DCHECK(node);
1109 
1110   // https://drafts.csswg.org/css-writing-modes-3/#bidi-control-codes-injection-table
1111   const ComputedStyle* style = node->Style();
1112   if (style->RtlOrdering() == EOrder::kLogical) {
1113     switch (style->GetUnicodeBidi()) {
1114       case UnicodeBidi::kNormal:
1115         break;
1116       case UnicodeBidi::kEmbed:
1117         EnterBidiContext(node, style, kLeftToRightEmbedCharacter,
1118                          kRightToLeftEmbedCharacter,
1119                          kPopDirectionalFormattingCharacter);
1120         break;
1121       case UnicodeBidi::kBidiOverride:
1122         EnterBidiContext(node, style, kLeftToRightOverrideCharacter,
1123                          kRightToLeftOverrideCharacter,
1124                          kPopDirectionalFormattingCharacter);
1125         break;
1126       case UnicodeBidi::kIsolate:
1127         EnterBidiContext(node, style, kLeftToRightIsolateCharacter,
1128                          kRightToLeftIsolateCharacter,
1129                          kPopDirectionalIsolateCharacter);
1130         break;
1131       case UnicodeBidi::kPlaintext:
1132         has_unicode_bidi_plain_text_ = true;
1133         EnterBidiContext(node, kFirstStrongIsolateCharacter,
1134                          kPopDirectionalIsolateCharacter);
1135         break;
1136       case UnicodeBidi::kIsolateOverride:
1137         EnterBidiContext(node, kFirstStrongIsolateCharacter,
1138                          kPopDirectionalIsolateCharacter);
1139         EnterBidiContext(node, style, kLeftToRightOverrideCharacter,
1140                          kRightToLeftOverrideCharacter,
1141                          kPopDirectionalFormattingCharacter);
1142         break;
1143     }
1144   }
1145 
1146   AppendOpaque(NGInlineItem::kOpenTag, node);
1147 
1148   if (!NeedsBoxInfo())
1149     return;
1150 
1151   // Set |ShouldCreateBoxFragment| of the parent box if needed.
1152   BoxInfo* current_box =
1153       &boxes_.emplace_back(items_->size() - 1, items_->back());
1154   if (boxes_.size() > 1) {
1155     BoxInfo* parent_box = std::prev(current_box);
1156     if (!parent_box->should_create_box_fragment &&
1157         parent_box->ShouldCreateBoxFragmentForChild(*current_box)) {
1158       parent_box->SetShouldCreateBoxFragment(items_);
1159     }
1160   }
1161 }
1162 
1163 template <typename OffsetMappingBuilder>
ExitBlock()1164 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ExitBlock() {
1165   Exit(nullptr);
1166 
1167   // Segment Break Transformation Rules[1] defines to keep trailing new lines,
1168   // but it will be removed in Phase II[2]. We prefer not to add trailing new
1169   // lines and collapsible spaces in Phase I.
1170   RemoveTrailingCollapsibleSpaceIfExists();
1171 }
1172 
1173 template <typename OffsetMappingBuilder>
ExitInline(LayoutObject * node)1174 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ExitInline(
1175     LayoutObject* node) {
1176   DCHECK(node);
1177 
1178   if (NeedsBoxInfo()) {
1179     BoxInfo* current_box = &boxes_.back();
1180     if (!current_box->should_create_box_fragment) {
1181       // Set ShouldCreateBoxFragment if this inline box is empty so that we can
1182       // compute its position/size correctly. Check this by looking for any
1183       // non-empty items after the last |kOpenTag|.
1184       const unsigned open_item_index = current_box->item_index;
1185       DCHECK_GE(items_->size(), open_item_index + 1);
1186       DCHECK_EQ((*items_)[open_item_index].Type(), NGInlineItem::kOpenTag);
1187       for (unsigned i = items_->size() - 1;; --i) {
1188         NGInlineItem& item = (*items_)[i];
1189         if (i == open_item_index) {
1190           DCHECK_EQ(i, current_box->item_index);
1191           // TODO(kojii): <area> element fails to hit-test when we don't cull.
1192           if (!IsA<HTMLAreaElement>(item.GetLayoutObject()->GetNode()))
1193             item.SetShouldCreateBoxFragment();
1194           break;
1195         }
1196         DCHECK_GT(i, current_box->item_index);
1197         if (item.IsEmptyItem()) {
1198           // float, abspos, collapsed space(<div>ab <span> </span>).
1199           // See editing/caret/empty_inlines.html
1200           // See also [1] for empty line box.
1201           // [1] https://drafts.csswg.org/css2/visuren.html#phantom-line-box
1202           continue;
1203         }
1204         if (item.IsCollapsibleSpaceOnly()) {
1205           // Because we can't collapse trailing spaces until next node, we
1206           // create box fragment for it: <div>ab<span> </span></div>
1207           // See editing/selection/mixed-editability-10.html
1208           continue;
1209         }
1210         break;
1211       }
1212     }
1213 
1214     boxes_.pop_back();
1215   }
1216 
1217   AppendOpaque(NGInlineItem::kCloseTag, node);
1218 
1219   Exit(node);
1220 }
1221 
1222 template <typename OffsetMappingBuilder>
Exit(LayoutObject * node)1223 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::Exit(
1224     LayoutObject* node) {
1225   while (!bidi_context_.IsEmpty() && bidi_context_.back().node == node) {
1226     AppendOpaque(NGInlineItem::kBidiControl, bidi_context_.back().exit);
1227     bidi_context_.pop_back();
1228   }
1229 }
1230 
1231 template <typename OffsetMappingBuilder>
MayBeBidiEnabled() const1232 bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::MayBeBidiEnabled()
1233     const {
1234   return !text_.Is8Bit() || HasBidiControls();
1235 }
1236 
1237 template <typename OffsetMappingBuilder>
1238 void NGInlineItemsBuilderTemplate<
DidFinishCollectInlines(NGInlineNodeData * data)1239     OffsetMappingBuilder>::DidFinishCollectInlines(NGInlineNodeData* data) {
1240   data->text_content = ToString();
1241 
1242   // Set |is_bidi_enabled_| for all UTF-16 strings for now, because at this
1243   // point the string may or may not contain RTL characters.
1244   // |SegmentText()| will analyze the text and reset |is_bidi_enabled_| if it
1245   // doesn't contain any RTL characters.
1246   data->is_bidi_enabled_ = MayBeBidiEnabled();
1247   // Note: Even if |IsEmptyInline()| is true, |text_| isn't empty, e.g. it
1248   // holds U+FFFC(ORC) for float or abspos.
1249   data->has_line_even_if_empty_ =
1250       IsEmptyInline() && block_flow_->HasLineIfEmpty();
1251   data->has_ruby_ = has_ruby_;
1252   data->is_empty_inline_ = IsEmptyInline();
1253   data->is_block_level_ = IsBlockLevel();
1254   data->changes_may_affect_earlier_lines_ = HasUnicodeBidiPlainText();
1255 }
1256 
1257 template <typename OffsetMappingBuilder>
SetIsSymbolMarker()1258 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::SetIsSymbolMarker() {
1259   DCHECK(!items_->IsEmpty());
1260   items_->back().SetIsSymbolMarker();
1261 }
1262 
1263 // Ensure this LayoutObject IsInLayoutNGInlineFormattingContext and does not
1264 // have associated NGPaintFragment.
1265 template <typename OffsetMappingBuilder>
ClearInlineFragment(LayoutObject * object)1266 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ClearInlineFragment(
1267     LayoutObject* object) {
1268   object->SetIsInLayoutNGInlineFormattingContext(true);
1269 }
1270 
1271 template <typename OffsetMappingBuilder>
ClearNeedsLayout(LayoutObject * object)1272 void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ClearNeedsLayout(
1273     LayoutObject* object) {
1274   // |CollectInlines()| for the pre-layout does not |ClearNeedsLayout|. It is
1275   // done during the actual layout because re-layout may not require
1276   // |CollectInlines()|.
1277   object->ClearNeedsCollectInlines();
1278   ClearInlineFragment(object);
1279 
1280   // Reset previous items if they cannot be reused to prevent stale items
1281   // for subsequent layouts. Items that can be reused have already been
1282   // added to the builder.
1283   if (object->IsText())
1284     To<LayoutText>(object)->ClearInlineItems();
1285 }
1286 
1287 template <typename OffsetMappingBuilder>
1288 void NGInlineItemsBuilderTemplate<
UpdateShouldCreateBoxFragment(LayoutInline * object)1289     OffsetMappingBuilder>::UpdateShouldCreateBoxFragment(LayoutInline* object) {
1290   object->UpdateShouldCreateBoxFragment();
1291 }
1292 
1293 // |NGOffsetMappingBuilder| doesn't change states of |LayoutObject|
1294 template <>
ClearNeedsLayout(LayoutObject * object)1295 void NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ClearNeedsLayout(
1296     LayoutObject* object) {}
1297 
1298 // |NGOffsetMappingBuilder| doesn't change states of |LayoutObject|
1299 template <>
ClearInlineFragment(LayoutObject *)1300 void NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ClearInlineFragment(
1301     LayoutObject*) {}
1302 
1303 // |NGOffsetMappingBuilder| doesn't change states of |LayoutInline|
1304 template <>
1305 void NGInlineItemsBuilderTemplate<
UpdateShouldCreateBoxFragment(LayoutInline *)1306     NGOffsetMappingBuilder>::UpdateShouldCreateBoxFragment(LayoutInline*) {}
1307 
1308 template class CORE_TEMPLATE_EXPORT
1309     NGInlineItemsBuilderTemplate<EmptyOffsetMappingBuilder>;
1310 template class CORE_TEMPLATE_EXPORT
1311     NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>;
1312 
1313 }  // namespace blink
1314