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