blob: dc1c754388f5cb4a0252315d97d34042bd75007b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/layout/ng/ng_layout_inline_items_builder.h"
#include "core/layout/LayoutObject.h"
#include "core/layout/ng/ng_inline_node.h"
#include "core/style/ComputedStyle.h"
namespace blink {
NGLayoutInlineItemsBuilder::~NGLayoutInlineItemsBuilder() {
DCHECK_EQ(0u, exits_.size());
DCHECK_EQ(text_.length(), items_->isEmpty() ? 0 : items_->back().EndOffset());
}
String NGLayoutInlineItemsBuilder::ToString() {
if (has_pending_newline_)
ProcessPendingNewline(emptyString(), nullptr);
return text_.toString();
}
// Determine "Ambiguous" East Asian Width is Wide or Narrow.
// Unicode East Asian Width
// http://unicode.org/reports/tr11/
static bool IsAmbiguosEastAsianWidthWide(const ComputedStyle* style) {
UScriptCode script = style->getFontDescription().script();
return script == USCRIPT_KATAKANA_OR_HIRAGANA ||
script == USCRIPT_SIMPLIFIED_HAN || script == USCRIPT_TRADITIONAL_HAN;
}
// Determine if a character has "Wide" East Asian Width.
static bool IsEastAsianWidthWide(UChar32 c, const ComputedStyle* style) {
UEastAsianWidth eaw = static_cast<UEastAsianWidth>(
u_getIntPropertyValue(c, UCHAR_EAST_ASIAN_WIDTH));
return eaw == U_EA_WIDE || eaw == U_EA_FULLWIDTH || eaw == U_EA_HALFWIDTH ||
(eaw == U_EA_AMBIGUOUS && style &&
IsAmbiguosEastAsianWidthWide(style));
}
// Determine whether a newline should be removed or not.
// CSS Text, Segment Break Transformation Rules
// https://drafts.csswg.org/css-text-3/#line-break-transform
static bool ShouldRemoveNewlineSlow(const StringBuilder& before,
const ComputedStyle* before_style,
const String& after,
unsigned after_index,
const ComputedStyle* after_style) {
// Remove if either before/after the newline is zeroWidthSpaceCharacter.
UChar32 last = 0;
if (!before.isEmpty()) {
last = before[before.length() - 1];
if (last == zeroWidthSpaceCharacter)
return true;
}
UChar32 next = 0;
if (after_index < after.length()) {
next = after[after_index];
if (next == zeroWidthSpaceCharacter)
return true;
}
// Logic below this point requires both before and after be 16 bits.
if (before.is8Bit() || after.is8Bit())
return false;
// Remove if East Asian Widths of both before/after the newline are Wide.
if (U16_IS_TRAIL(last) && before.length() >= 2) {
UChar last_last = before[before.length() - 2];
if (U16_IS_LEAD(last_last))
last = U16_GET_SUPPLEMENTARY(last_last, last);
}
if (IsEastAsianWidthWide(last, before_style)) {
if (U16_IS_LEAD(next) && after_index + 1 < after.length()) {
UChar next_next = after[after_index + 1];
if (U16_IS_TRAIL(next_next))
next = U16_GET_SUPPLEMENTARY(next, next_next);
}
if (IsEastAsianWidthWide(next, after_style))
return true;
}
return false;
}
static bool ShouldRemoveNewline(const StringBuilder& before,
const ComputedStyle* before_style,
const String& after,
unsigned after_index,
const ComputedStyle* after_style) {
// All characters before/after removable newline are 16 bits.
return (!before.is8Bit() || !after.is8Bit()) &&
ShouldRemoveNewlineSlow(before, before_style, after, after_index,
after_style);
}
void NGLayoutInlineItemsBuilder::Append(const String& string,
const ComputedStyle* style) {
if (string.isEmpty()) {
unsigned offset = text_.length();
items_->append(NGLayoutInlineItem(offset, offset, style));
return;
}
if (has_pending_newline_)
ProcessPendingNewline(string, style);
EWhiteSpace whitespace = style->whiteSpace();
bool preserve_newline =
ComputedStyle::preserveNewline(whitespace) && !is_svgtext_;
bool collapse_whitespace = ComputedStyle::collapseWhiteSpace(whitespace);
unsigned start_offset = text_.length();
if (!collapse_whitespace) {
text_.append(string);
is_last_collapsible_space_ = false;
} else {
text_.reserveCapacity(string.length());
for (unsigned i = 0; i < string.length(); i++) {
UChar c = string[i];
bool is_collapsible_space;
if (c == newlineCharacter) {
RemoveTrailingCollapsibleSpace(&start_offset);
if (preserve_newline) {
text_.append(c);
// Remove collapsible spaces immediately following a newline.
is_last_collapsible_space_ = true;
continue;
}
if (i + 1 == string.length()) {
// If at the end of string, process this newline on the next Append.
has_pending_newline_ = true;
break;
}
if (ShouldRemoveNewline(text_, style, string, i + 1, style))
continue;
is_collapsible_space = true;
} else {
is_collapsible_space = c == spaceCharacter || c == tabulationCharacter;
}
if (!is_collapsible_space) {
text_.append(c);
is_last_collapsible_space_ = false;
} else if (!is_last_collapsible_space_) {
text_.append(spaceCharacter);
is_last_collapsible_space_ = true;
}
}
}
items_->append(NGLayoutInlineItem(start_offset, text_.length(), style));
}
void NGLayoutInlineItemsBuilder::Append(UChar character) {
DCHECK(character != spaceCharacter && character != tabulationCharacter &&
character != newlineCharacter && character != zeroWidthSpaceCharacter);
AppendAsOpaqueToSpaceCollapsing(character);
is_last_collapsible_space_ = false;
}
void NGLayoutInlineItemsBuilder::AppendAsOpaqueToSpaceCollapsing(
UChar character) {
if (has_pending_newline_)
ProcessPendingNewline(emptyString(), nullptr);
text_.append(character);
unsigned end_offset = text_.length();
items_->append(NGLayoutInlineItem(end_offset - 1, end_offset, nullptr));
}
void NGLayoutInlineItemsBuilder::ProcessPendingNewline(
const String& string,
const ComputedStyle* style) {
DCHECK(has_pending_newline_);
NGLayoutInlineItem& item = items_->back();
if (!ShouldRemoveNewline(text_, item.Style(), string, 0, style)) {
text_.append(spaceCharacter);
item.SetEndOffset(text_.length());
}
// Remove spaces following a newline even when the newline was removed.
is_last_collapsible_space_ = true;
has_pending_newline_ = false;
}
void NGLayoutInlineItemsBuilder::RemoveTrailingCollapsibleSpace(
unsigned* offset) {
if (!text_.isEmpty() && is_last_collapsible_space_) {
DCHECK_EQ(spaceCharacter, text_[text_.length() - 1]);
text_.resize(text_.length() - 1);
is_last_collapsible_space_ = false;
if (*offset > text_.length())
*offset = text_.length();
}
}
void NGLayoutInlineItemsBuilder::AppendBidiControl(const ComputedStyle* style,
UChar ltr,
UChar rtl) {
AppendAsOpaqueToSpaceCollapsing(style->direction() == RTL ? rtl : ltr);
}
void NGLayoutInlineItemsBuilder::EnterBlock(const ComputedStyle* style) {
// Handle bidi-override on the block itself.
// Isolate and embed values are enforced by default and redundant on the block
// elements.
// Plaintext and direction are handled as the paragraph level by
// NGBidiParagraph::SetParagraph().
if (style->unicodeBidi() == Override ||
style->unicodeBidi() == IsolateOverride) {
AppendBidiControl(style, leftToRightOverrideCharacter,
rightToLeftOverrideCharacter);
Enter(nullptr, popDirectionalFormattingCharacter);
}
}
void NGLayoutInlineItemsBuilder::EnterInline(LayoutObject* node) {
// https://drafts.csswg.org/css-writing-modes-3/#bidi-control-codes-injection-table
const ComputedStyle* style = node->style();
switch (style->unicodeBidi()) {
case UBNormal:
break;
case Embed:
AppendBidiControl(style, leftToRightEmbedCharacter,
rightToLeftEmbedCharacter);
Enter(node, popDirectionalFormattingCharacter);
break;
case Override:
AppendBidiControl(style, leftToRightOverrideCharacter,
rightToLeftOverrideCharacter);
Enter(node, popDirectionalFormattingCharacter);
break;
case Isolate:
AppendBidiControl(style, leftToRightIsolateCharacter,
rightToLeftIsolateCharacter);
Enter(node, popDirectionalIsolateCharacter);
break;
case Plaintext:
AppendAsOpaqueToSpaceCollapsing(firstStrongIsolateCharacter);
Enter(node, popDirectionalIsolateCharacter);
break;
case IsolateOverride:
AppendAsOpaqueToSpaceCollapsing(firstStrongIsolateCharacter);
AppendBidiControl(style, leftToRightOverrideCharacter,
rightToLeftOverrideCharacter);
Enter(node, popDirectionalIsolateCharacter);
Enter(node, popDirectionalFormattingCharacter);
break;
}
}
void NGLayoutInlineItemsBuilder::Enter(LayoutObject* node,
UChar character_to_exit) {
exits_.append(OnExitNode{node, character_to_exit});
has_bidi_controls_ = true;
}
void NGLayoutInlineItemsBuilder::ExitBlock() {
Exit(nullptr);
}
void NGLayoutInlineItemsBuilder::ExitInline(LayoutObject* node) {
DCHECK(node);
Exit(node);
}
void NGLayoutInlineItemsBuilder::Exit(LayoutObject* node) {
while (!exits_.isEmpty() && exits_.back().node == node) {
AppendAsOpaqueToSpaceCollapsing(exits_.back().character);
exits_.pop_back();
}
}
} // namespace blink