blob: 61dc7e98aec4b28de5b90189590992bb5d696cc5 [file] [log] [blame]
/*
* Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/editing/commands/insert_paragraph_separator_command.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/commands/delete_selection_options.h"
#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
#include "third_party/blink/renderer/core/editing/commands/insert_line_break_command.h"
#include "third_party/blink/renderer/core/editing/editing_style.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/editing/visible_selection.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/html/html_br_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_quote_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
namespace blink {
// When inserting a new line, we want to avoid nesting empty divs if we can.
// Otherwise, when pasting, it's easy to have each new line be a div deeper than
// the previous. E.g., in the case below, we want to insert at ^ instead of |.
// <div>foo<div>bar</div>|</div>^
static Element* HighestVisuallyEquivalentDivBelowRoot(Element* start_block) {
Element* cur_block = start_block;
// We don't want to return a root node (if it happens to be a div, e.g., in a
// document fragment) because there are no siblings for us to append to.
while (!cur_block->nextSibling() &&
IsA<HTMLDivElement>(*cur_block->parentElement()) &&
cur_block->parentElement()->parentElement()) {
if (cur_block->parentElement()->hasAttributes())
break;
cur_block = cur_block->parentElement();
}
return cur_block;
}
static bool InSameBlock(const VisiblePosition& a, const VisiblePosition& b) {
DCHECK(a.IsValid()) << a;
DCHECK(b.IsValid()) << b;
return !a.IsNull() &&
EnclosingBlock(a.DeepEquivalent().ComputeContainerNode()) ==
EnclosingBlock(b.DeepEquivalent().ComputeContainerNode());
}
InsertParagraphSeparatorCommand::InsertParagraphSeparatorCommand(
Document& document,
bool must_use_default_paragraph_element,
bool paste_blockquote_into_unquoted_area)
: CompositeEditCommand(document),
must_use_default_paragraph_element_(must_use_default_paragraph_element),
paste_blockquote_into_unquoted_area_(
paste_blockquote_into_unquoted_area) {}
bool InsertParagraphSeparatorCommand::PreservesTypingStyle() const {
return true;
}
void InsertParagraphSeparatorCommand::CalculateStyleBeforeInsertion(
const Position& pos) {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetDocument().Lifecycle());
// It is only important to set a style to apply later if we're at the
// boundaries of a paragraph. Otherwise, content that is moved as part of the
// work of the command will lend their styles to the new paragraph without any
// extra work needed.
VisiblePosition visible_pos = CreateVisiblePosition(pos);
if (!IsStartOfParagraph(visible_pos) && !IsEndOfParagraph(visible_pos))
return;
DCHECK(pos.IsNotNull());
style_ = MakeGarbageCollected<EditingStyle>(pos);
style_->MergeTypingStyle(pos.GetDocument());
}
void InsertParagraphSeparatorCommand::ApplyStyleAfterInsertion(
Element* original_enclosing_block,
EditingState* editing_state) {
// Not only do we break out of header tags, but we also do not preserve the
// typing style, in order to match other browsers.
if (original_enclosing_block->HasTagName(html_names::kH1Tag) ||
original_enclosing_block->HasTagName(html_names::kH2Tag) ||
original_enclosing_block->HasTagName(html_names::kH3Tag) ||
original_enclosing_block->HasTagName(html_names::kH4Tag) ||
original_enclosing_block->HasTagName(html_names::kH5Tag)) {
return;
}
if (!style_)
return;
style_->PrepareToApplyAt(EndingVisibleSelection().Start());
if (!style_->IsEmpty())
ApplyStyle(style_.Get(), editing_state);
}
bool InsertParagraphSeparatorCommand::ShouldUseDefaultParagraphElement(
Element* enclosing_block) const {
DCHECK(!GetDocument().NeedsLayoutTreeUpdate());
if (must_use_default_paragraph_element_)
return true;
// Assumes that if there was a range selection, it was already deleted.
if (!IsEndOfBlock(EndingVisibleSelection().VisibleStart()))
return false;
return enclosing_block->HasTagName(html_names::kH1Tag) ||
enclosing_block->HasTagName(html_names::kH2Tag) ||
enclosing_block->HasTagName(html_names::kH3Tag) ||
enclosing_block->HasTagName(html_names::kH4Tag) ||
enclosing_block->HasTagName(html_names::kH5Tag);
}
void InsertParagraphSeparatorCommand::GetAncestorsInsideBlock(
const Node* insertion_node,
Element* outer_block,
HeapVector<Member<Element>>& ancestors) {
ancestors.clear();
// Build up list of ancestors elements between the insertion node and the
// outer block.
if (insertion_node != outer_block) {
for (Element* n = insertion_node->parentElement(); n && n != outer_block;
n = n->parentElement())
ancestors.push_back(n);
}
}
Element* InsertParagraphSeparatorCommand::CloneHierarchyUnderNewBlock(
const HeapVector<Member<Element>>& ancestors,
Element* block_to_insert,
EditingState* editing_state) {
// Make clones of ancestors in between the start node and the start block.
Element* parent = block_to_insert;
for (wtf_size_t i = ancestors.size(); i != 0; --i) {
Element& child = ancestors[i - 1]->CloneWithoutChildren();
// It should always be okay to remove id from the cloned elements, since the
// originals are not deleted.
child.removeAttribute(html_names::kIdAttr);
AppendNode(&child, parent, editing_state);
if (editing_state->IsAborted())
return nullptr;
parent = &child;
}
return parent;
}
void InsertParagraphSeparatorCommand::DoApply(EditingState* editing_state) {
// TODO(editing-dev): We shouldn't construct an
// InsertParagraphSeparatorCommand with none or invalid selection.
const VisibleSelection& visible_selection = EndingVisibleSelection();
if (visible_selection.IsNone() ||
!visible_selection.IsValidFor(GetDocument()))
return;
Position insertion_position = visible_selection.Start();
TextAffinity affinity = visible_selection.Affinity();
// Delete the current selection.
if (EndingSelection().IsRange()) {
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
CalculateStyleBeforeInsertion(insertion_position);
if (!DeleteSelection(editing_state, DeleteSelectionOptions::NormalDelete()))
return;
const VisibleSelection& visble_selection_after_delete =
EndingVisibleSelection();
insertion_position = visble_selection_after_delete.Start();
affinity = visble_selection_after_delete.Affinity();
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// FIXME: The parentAnchoredEquivalent conversion needs to be moved into
// enclosingBlock.
Element* start_block = EnclosingBlock(
insertion_position.ParentAnchoredEquivalent().ComputeContainerNode());
Node* list_child_node = EnclosingListChild(
insertion_position.ParentAnchoredEquivalent().ComputeContainerNode());
auto* list_child = DynamicTo<HTMLElement>(list_child_node);
Position canonical_pos =
CreateVisiblePosition(insertion_position).DeepEquivalent();
if (!start_block || !start_block->NonShadowBoundaryParentNode() ||
IsTableCell(start_block) ||
IsA<HTMLFormElement>(*start_block)
// FIXME: If the node is hidden, we don't have a canonical position so we
// will do the wrong thing for tables and <hr>.
// https://bugs.webkit.org/show_bug.cgi?id=40342
|| (!canonical_pos.IsNull() &&
IsDisplayInsideTable(canonical_pos.AnchorNode())) ||
(!canonical_pos.IsNull() &&
IsA<HTMLHRElement>(*canonical_pos.AnchorNode()))) {
ApplyCommandToComposite(
MakeGarbageCollected<InsertLineBreakCommand>(GetDocument()),
editing_state);
return;
}
// Use the leftmost candidate.
insertion_position = MostBackwardCaretPosition(insertion_position);
if (!IsVisuallyEquivalentCandidate(insertion_position))
insertion_position = MostForwardCaretPosition(insertion_position);
// Adjust the insertion position after the delete
const Position original_insertion_position = insertion_position;
const Element* enclosing_anchor =
EnclosingAnchorElement(original_insertion_position);
insertion_position =
PositionAvoidingSpecialElementBoundary(insertion_position, editing_state);
if (editing_state->IsAborted())
return;
// InsertTextCommandTest.AnchorElementWithBlockCrash reaches here.
ABORT_EDITING_COMMAND_IF(!start_block->parentNode());
if (list_child == enclosing_anchor) {
// |positionAvoidingSpecialElementBoundary()| creates new A element and
// move to another place.
list_child =
To<HTMLElement>(EnclosingAnchorElement(original_insertion_position));
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
CalculateStyleBeforeInsertion(insertion_position);
//---------------------------------------------------------------------
// Handle special case of typing return on an empty list item
if (BreakOutOfEmptyListItem(editing_state) || editing_state->IsAborted())
return;
//---------------------------------------------------------------------
// Prepare for more general cases.
// Create block to be inserted.
bool nest_new_block = false;
Element* block_to_insert = nullptr;
if (IsRootEditableElement(*start_block)) {
block_to_insert = CreateDefaultParagraphElement(GetDocument());
nest_new_block = true;
} else if (ShouldUseDefaultParagraphElement(start_block)) {
block_to_insert = CreateDefaultParagraphElement(GetDocument());
} else {
block_to_insert = &start_block->CloneWithoutChildren();
}
VisiblePosition visible_pos =
CreateVisiblePosition(insertion_position, affinity);
bool is_first_in_block = IsStartOfBlock(visible_pos);
bool is_last_in_block = IsEndOfBlock(visible_pos);
//---------------------------------------------------------------------
// Handle case when position is in the last visible position in its block,
// including when the block is empty.
if (is_last_in_block) {
if (nest_new_block) {
if (is_first_in_block && !LineBreakExistsAtVisiblePosition(visible_pos)) {
// The block is empty. Create an empty block to
// represent the paragraph that we're leaving.
HTMLElement* extra_block = CreateDefaultParagraphElement(GetDocument());
AppendNode(extra_block, start_block, editing_state);
if (editing_state->IsAborted())
return;
AppendBlockPlaceholder(extra_block, editing_state);
if (editing_state->IsAborted())
return;
}
AppendNode(block_to_insert, start_block, editing_state);
if (editing_state->IsAborted())
return;
} else {
// We can get here if we pasted a copied portion of a blockquote with a
// newline at the end and are trying to paste it into an unquoted area. We
// then don't want the newline within the blockquote or else it will also
// be quoted.
if (paste_blockquote_into_unquoted_area_) {
if (auto* highest_blockquote =
To<HTMLQuoteElement>(HighestEnclosingNodeOfType(
canonical_pos, &IsMailHTMLBlockquoteElement)))
start_block = highest_blockquote;
}
if (list_child && list_child != start_block) {
Element& list_child_to_insert = list_child->CloneWithoutChildren();
AppendNode(block_to_insert, &list_child_to_insert, editing_state);
if (editing_state->IsAborted())
return;
InsertNodeAfter(&list_child_to_insert, list_child, editing_state);
} else {
// Most of the time we want to stay at the nesting level of the
// startBlock (e.g., when nesting within lists). However, for div nodes,
// this can result in nested div tags that are hard to break out of.
Element* sibling_element = start_block;
if (IsA<HTMLDivElement>(*block_to_insert))
sibling_element = HighestVisuallyEquivalentDivBelowRoot(start_block);
InsertNodeAfter(block_to_insert, sibling_element, editing_state);
}
if (editing_state->IsAborted())
return;
}
// Recreate the same structure in the new paragraph.
HeapVector<Member<Element>> ancestors;
GetAncestorsInsideBlock(
PositionOutsideTabSpan(insertion_position).AnchorNode(), start_block,
ancestors);
Element* parent =
CloneHierarchyUnderNewBlock(ancestors, block_to_insert, editing_state);
if (editing_state->IsAborted())
return;
AppendBlockPlaceholder(parent, editing_state);
if (editing_state->IsAborted())
return;
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::FirstPositionInNode(*parent))
.Build()));
return;
}
//---------------------------------------------------------------------
// Handle case when position is in the first visible position in its block,
// and similar case where previous position is in another, presumeably nested,
// block.
if (is_first_in_block ||
!InSameBlock(visible_pos, PreviousPositionOf(visible_pos))) {
Node* ref_node = nullptr;
insertion_position = PositionOutsideTabSpan(insertion_position);
if (is_first_in_block && !nest_new_block) {
if (list_child && list_child != start_block) {
Element& list_child_to_insert = list_child->CloneWithoutChildren();
AppendNode(block_to_insert, &list_child_to_insert, editing_state);
if (editing_state->IsAborted())
return;
InsertNodeBefore(&list_child_to_insert, list_child, editing_state);
if (editing_state->IsAborted())
return;
} else {
ref_node = start_block;
}
} else if (is_first_in_block && nest_new_block) {
// startBlock should always have children, otherwise isLastInBlock would
// be true and it's handled above.
DCHECK(start_block->HasChildren());
ref_node = start_block->firstChild();
} else if (insertion_position.AnchorNode() == start_block &&
nest_new_block) {
ref_node = NodeTraversal::ChildAt(
*start_block, insertion_position.ComputeEditingOffset());
DCHECK(ref_node); // must be true or we'd be in the end of block case
} else {
ref_node = insertion_position.AnchorNode();
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// find ending selection position easily before inserting the paragraph
insertion_position = MostForwardCaretPosition(insertion_position);
if (ref_node) {
InsertNodeBefore(block_to_insert, ref_node, editing_state);
if (editing_state->IsAborted())
return;
}
// Recreate the same structure in the new paragraph.
HeapVector<Member<Element>> ancestors;
insertion_position = PositionAvoidingSpecialElementBoundary(
PositionOutsideTabSpan(insertion_position), editing_state);
if (editing_state->IsAborted())
return;
GetAncestorsInsideBlock(insertion_position.AnchorNode(), start_block,
ancestors);
Element* placeholder =
CloneHierarchyUnderNewBlock(ancestors, block_to_insert, editing_state);
if (editing_state->IsAborted())
return;
AppendBlockPlaceholder(placeholder, editing_state);
if (editing_state->IsAborted())
return;
// In this case, we need to set the new ending selection.
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(insertion_position)
.Build()));
return;
}
//---------------------------------------------------------------------
// Handle the (more complicated) general case,
// All of the content in the current block after visiblePos is
// about to be wrapped in a new paragraph element. Add a br before
// it if visiblePos is at the start of a paragraph so that the
// content will move down a line.
if (IsStartOfParagraph(visible_pos)) {
auto* br = MakeGarbageCollected<HTMLBRElement>(GetDocument());
InsertNodeAt(br, insertion_position, editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
insertion_position = Position::InParentAfterNode(*br);
visible_pos = CreateVisiblePosition(insertion_position);
// If the insertion point is a break element, there is nothing else
// we need to do.
if (visible_pos.IsNotNull() &&
visible_pos.DeepEquivalent().AnchorNode()->GetLayoutObject()->IsBR()) {
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(insertion_position)
.Build()));
return;
}
}
// Move downstream. Typing style code will take care of carrying along the
// style of the upstream position.
insertion_position = MostForwardCaretPosition(insertion_position);
// At this point, the insertionPosition's node could be a container, and we
// want to make sure we include all of the correct nodes when building the
// ancestor list. So this needs to be the deepest representation of the
// position before we walk the DOM tree.
VisiblePosition visible_insertion_position =
CreateVisiblePosition(insertion_position);
ABORT_EDITING_COMMAND_IF(visible_insertion_position.IsNull());
insertion_position =
PositionOutsideTabSpan(visible_insertion_position.DeepEquivalent());
// If the returned position lies either at the end or at the start of an
// element that is ignored by editing we should move to its upstream or
// downstream position.
if (EditingIgnoresContent(*insertion_position.AnchorNode())) {
if (insertion_position.AtLastEditingPositionForNode())
insertion_position = MostForwardCaretPosition(insertion_position);
else if (insertion_position.AtFirstEditingPositionForNode())
insertion_position = MostBackwardCaretPosition(insertion_position);
}
ABORT_EDITING_COMMAND_IF(!IsEditablePosition(insertion_position));
// Make sure we do not cause a rendered space to become unrendered.
// FIXME: We need the affinity for pos, but mostForwardCaretPosition does not
// give it
Position leading_whitespace = LeadingCollapsibleWhitespacePosition(
insertion_position, TextAffinity::kDefault);
// FIXME: leadingCollapsibleWhitespacePosition is returning the position
// before preserved newlines for positions after the preserved newline,
// causing the newline to be turned into a nbsp.
if (leading_whitespace.IsNotNull()) {
if (auto* text_node = DynamicTo<Text>(leading_whitespace.AnchorNode())) {
DCHECK(!text_node->GetLayoutObject() ||
text_node->GetLayoutObject()->Style()->CollapseWhiteSpace())
<< text_node;
ReplaceTextInNode(text_node,
leading_whitespace.ComputeOffsetInContainerNode(), 1,
NonBreakingSpaceString());
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
}
}
// Split at pos if in the middle of a text node.
Position position_after_split;
if (insertion_position.IsOffsetInAnchor()) {
if (auto* text_node =
DynamicTo<Text>(insertion_position.ComputeContainerNode())) {
int text_offset = insertion_position.OffsetInContainerNode();
bool at_end = static_cast<unsigned>(text_offset) >= text_node->length();
if (text_offset > 0 && !at_end) {
SplitTextNode(text_node, text_offset);
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
position_after_split = Position::FirstPositionInNode(*text_node);
insertion_position =
Position(text_node->previousSibling(), text_offset);
}
}
}
// If we got detached due to mutation events, just bail out.
if (!start_block->parentNode())
return;
// Put the added block in the tree.
if (nest_new_block) {
AppendNode(block_to_insert, start_block, editing_state);
} else if (list_child && list_child != start_block) {
Element& list_child_to_insert = list_child->CloneWithoutChildren();
AppendNode(block_to_insert, &list_child_to_insert, editing_state);
if (editing_state->IsAborted())
return;
InsertNodeAfter(&list_child_to_insert, list_child, editing_state);
} else {
InsertNodeAfter(block_to_insert, start_block, editing_state);
}
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
visible_pos = CreateVisiblePosition(insertion_position);
// If the paragraph separator was inserted at the end of a paragraph, an empty
// line must be created. All of the nodes, starting at visiblePos, are about
// to be added to the new paragraph element. If the first node to be inserted
// won't be one that will hold an empty line open, add a br.
if (IsEndOfParagraph(visible_pos) &&
!LineBreakExistsAtVisiblePosition(visible_pos)) {
AppendNode(MakeGarbageCollected<HTMLBRElement>(GetDocument()),
block_to_insert, editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
}
// Move the start node and the siblings of the start node.
if (CreateVisiblePosition(insertion_position).DeepEquivalent() !=
VisiblePosition::BeforeNode(*block_to_insert).DeepEquivalent()) {
Node* n;
if (insertion_position.ComputeContainerNode() == start_block) {
n = insertion_position.ComputeNodeAfterPosition();
} else {
Node* split_to = insertion_position.ComputeContainerNode();
if (split_to->IsTextNode() &&
insertion_position.OffsetInContainerNode() >=
CaretMaxOffset(split_to))
split_to = NodeTraversal::Next(*split_to, start_block);
if (split_to)
SplitTreeToNode(split_to, start_block);
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
for (n = start_block->firstChild(); n; n = n->nextSibling()) {
VisiblePosition before_node_position = VisiblePosition::BeforeNode(*n);
if (!before_node_position.IsNull() &&
ComparePositions(CreateVisiblePosition(insertion_position),
before_node_position) <= 0)
break;
}
}
MoveRemainingSiblingsToNewParent(n, block_to_insert, block_to_insert,
editing_state);
if (editing_state->IsAborted())
return;
}
// Handle whitespace that occurs after the split
if (position_after_split.IsNotNull()) {
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
if (!IsRenderedCharacter(position_after_split)) {
// Clear out all whitespace and insert one non-breaking space
DCHECK(!position_after_split.ComputeContainerNode()->GetLayoutObject() ||
position_after_split.ComputeContainerNode()
->GetLayoutObject()
->Style()
->CollapseWhiteSpace())
<< position_after_split;
DeleteInsignificantTextDownstream(position_after_split);
if (auto* is_text_node =
DynamicTo<Text>(position_after_split.AnchorNode())) {
InsertTextIntoNode(
To<Text>(position_after_split.ComputeContainerNode()), 0,
NonBreakingSpaceString());
}
}
}
SetEndingSelection(SelectionForUndoStep::From(
SelectionInDOMTree::Builder()
.Collapse(Position::FirstPositionInNode(*block_to_insert))
.Build()));
ApplyStyleAfterInsertion(start_block, editing_state);
}
void InsertParagraphSeparatorCommand::Trace(Visitor* visitor) const {
visitor->Trace(style_);
CompositeEditCommand::Trace(visitor);
}
} // namespace blink