1 // Copyright (c) 2012 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 "content/renderer/accessibility/renderer_accessibility_complete.h" 6 7 #include <queue> 8 9 #include "base/bind.h" 10 #include "base/message_loop/message_loop.h" 11 #include "content/renderer/accessibility/accessibility_node_serializer.h" 12 #include "content/renderer/render_view_impl.h" 13 #include "third_party/WebKit/public/web/WebAXObject.h" 14 #include "third_party/WebKit/public/web/WebDocument.h" 15 #include "third_party/WebKit/public/web/WebFrame.h" 16 #include "third_party/WebKit/public/web/WebInputElement.h" 17 #include "third_party/WebKit/public/web/WebNode.h" 18 #include "third_party/WebKit/public/web/WebView.h" 19 20 using blink::WebAXObject; 21 using blink::WebDocument; 22 using blink::WebFrame; 23 using blink::WebNode; 24 using blink::WebPoint; 25 using blink::WebRect; 26 using blink::WebSize; 27 using blink::WebView; 28 29 namespace content { 30 31 RendererAccessibilityComplete::RendererAccessibilityComplete( 32 RenderViewImpl* render_view) 33 : RendererAccessibility(render_view), 34 weak_factory_(this), 35 browser_root_(NULL), 36 last_scroll_offset_(gfx::Size()), 37 ack_pending_(false) { 38 WebAXObject::enableAccessibility(); 39 40 #if !defined(OS_ANDROID) 41 // Skip inline text boxes on Android - since there are no native Android 42 // APIs that compute the bounds of a range of text, it's a waste to 43 // include these in the AX tree. 44 WebAXObject::enableInlineTextBoxAccessibility(); 45 #endif 46 47 const WebDocument& document = GetMainDocument(); 48 if (!document.isNull()) { 49 // It's possible that the webview has already loaded a webpage without 50 // accessibility being enabled. Initialize the browser's cached 51 // accessibility tree by sending it a notification. 52 HandleWebAccessibilityEvent(document.accessibilityObject(), 53 blink::WebAXEventLayoutComplete); 54 } 55 } 56 57 RendererAccessibilityComplete::~RendererAccessibilityComplete() { 58 if (browser_root_) { 59 ClearBrowserTreeNode(browser_root_); 60 browser_id_map_.erase(browser_root_->id); 61 delete browser_root_; 62 } 63 DCHECK(browser_id_map_.empty()); 64 } 65 66 bool RendererAccessibilityComplete::OnMessageReceived( 67 const IPC::Message& message) { 68 bool handled = true; 69 IPC_BEGIN_MESSAGE_MAP(RendererAccessibilityComplete, message) 70 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetFocus, OnSetFocus) 71 IPC_MESSAGE_HANDLER(AccessibilityMsg_DoDefaultAction, 72 OnDoDefaultAction) 73 IPC_MESSAGE_HANDLER(AccessibilityMsg_Events_ACK, 74 OnEventsAck) 75 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToMakeVisible, 76 OnScrollToMakeVisible) 77 IPC_MESSAGE_HANDLER(AccessibilityMsg_ScrollToPoint, 78 OnScrollToPoint) 79 IPC_MESSAGE_HANDLER(AccessibilityMsg_SetTextSelection, 80 OnSetTextSelection) 81 IPC_MESSAGE_HANDLER(AccessibilityMsg_FatalError, OnFatalError) 82 IPC_MESSAGE_UNHANDLED(handled = false) 83 IPC_END_MESSAGE_MAP() 84 return handled; 85 } 86 87 void RendererAccessibilityComplete::FocusedNodeChanged(const WebNode& node) { 88 const WebDocument& document = GetMainDocument(); 89 if (document.isNull()) 90 return; 91 92 if (node.isNull()) { 93 // When focus is cleared, implicitly focus the document. 94 // TODO(dmazzoni): Make WebKit send this notification instead. 95 HandleWebAccessibilityEvent(document.accessibilityObject(), 96 blink::WebAXEventBlur); 97 } 98 } 99 100 void RendererAccessibilityComplete::DidFinishLoad(blink::WebFrame* frame) { 101 const WebDocument& document = GetMainDocument(); 102 if (document.isNull()) 103 return; 104 105 // Check to see if the root accessibility object has changed, to work 106 // around WebKit bugs that cause AXObjectCache to be cleared 107 // unnecessarily. 108 // TODO(dmazzoni): remove this once rdar://5794454 is fixed. 109 WebAXObject new_root = document.accessibilityObject(); 110 if (!browser_root_ || new_root.axID() != browser_root_->id) 111 HandleWebAccessibilityEvent(new_root, blink::WebAXEventLayoutComplete); 112 } 113 114 void RendererAccessibilityComplete::HandleWebAccessibilityEvent( 115 const blink::WebAXObject& obj, 116 blink::WebAXEvent event) { 117 const WebDocument& document = GetMainDocument(); 118 if (document.isNull()) 119 return; 120 121 gfx::Size scroll_offset = document.frame()->scrollOffset(); 122 if (scroll_offset != last_scroll_offset_) { 123 // Make sure the browser is always aware of the scroll position of 124 // the root document element by posting a generic notification that 125 // will update it. 126 // TODO(dmazzoni): remove this as soon as 127 // https://bugs.webkit.org/show_bug.cgi?id=73460 is fixed. 128 last_scroll_offset_ = scroll_offset; 129 if (!obj.equals(document.accessibilityObject())) { 130 HandleWebAccessibilityEvent( 131 document.accessibilityObject(), 132 blink::WebAXEventLayoutComplete); 133 } 134 } 135 136 // Add the accessibility object to our cache and ensure it's valid. 137 AccessibilityHostMsg_EventParams acc_event; 138 acc_event.id = obj.axID(); 139 acc_event.event_type = event; 140 141 // Discard duplicate accessibility events. 142 for (uint32 i = 0; i < pending_events_.size(); ++i) { 143 if (pending_events_[i].id == acc_event.id && 144 pending_events_[i].event_type == 145 acc_event.event_type) { 146 return; 147 } 148 } 149 pending_events_.push_back(acc_event); 150 151 if (!ack_pending_ && !weak_factory_.HasWeakPtrs()) { 152 // When no accessibility events are in-flight post a task to send 153 // the events to the browser. We use PostTask so that we can queue 154 // up additional events. 155 base::MessageLoop::current()->PostTask( 156 FROM_HERE, 157 base::Bind(&RendererAccessibilityComplete:: 158 SendPendingAccessibilityEvents, 159 weak_factory_.GetWeakPtr())); 160 } 161 } 162 163 RendererAccessibilityComplete::BrowserTreeNode::BrowserTreeNode() : id(0) {} 164 165 RendererAccessibilityComplete::BrowserTreeNode::~BrowserTreeNode() {} 166 167 void RendererAccessibilityComplete::SendPendingAccessibilityEvents() { 168 const WebDocument& document = GetMainDocument(); 169 if (document.isNull()) 170 return; 171 172 if (pending_events_.empty()) 173 return; 174 175 if (render_view_->is_swapped_out()) 176 return; 177 178 ack_pending_ = true; 179 180 // Make a copy of the events, because it's possible that 181 // actions inside this loop will cause more events to be 182 // queued up. 183 std::vector<AccessibilityHostMsg_EventParams> src_events = 184 pending_events_; 185 pending_events_.clear(); 186 187 // Generate an event message from each WebKit event. 188 std::vector<AccessibilityHostMsg_EventParams> event_msgs; 189 190 // Loop over each event and generate an updated event message. 191 for (size_t i = 0; i < src_events.size(); ++i) { 192 AccessibilityHostMsg_EventParams& event = 193 src_events[i]; 194 195 WebAXObject obj = document.accessibilityObjectFromID( 196 event.id); 197 if (!obj.updateBackingStoreAndCheckValidity()) 198 continue; 199 200 // When we get a "selected children changed" event, WebKit 201 // doesn't also send us events for each child that changed 202 // selection state, so make sure we re-send that whole subtree. 203 if (event.event_type == 204 blink::WebAXEventSelectedChildrenChanged) { 205 base::hash_map<int32, BrowserTreeNode*>::iterator iter = 206 browser_id_map_.find(obj.axID()); 207 if (iter != browser_id_map_.end()) 208 ClearBrowserTreeNode(iter->second); 209 } 210 211 // The browser may not have this object yet, for example if we get a 212 // event on an object that was recently added, or if we get a 213 // event on a node before the page has loaded. Work our way 214 // up the parent chain until we find a node the browser has, or until 215 // we reach the root. 216 WebAXObject root_object = document.accessibilityObject(); 217 int root_id = root_object.axID(); 218 while (browser_id_map_.find(obj.axID()) == browser_id_map_.end() && 219 !obj.isDetached() && 220 obj.axID() != root_id) { 221 obj = obj.parentObject(); 222 if (event.event_type == 223 blink::WebAXEventChildrenChanged) { 224 event.id = obj.axID(); 225 } 226 } 227 228 if (obj.isDetached()) { 229 #ifndef NDEBUG 230 if (logging_) 231 LOG(WARNING) << "Got event on object that is invalid or has" 232 << " invalid ancestor. Id: " << obj.axID(); 233 #endif 234 continue; 235 } 236 237 // Another potential problem is that this event may be on an 238 // object that is detached from the tree. Determine if this node is not a 239 // child of its parent, and if so move the event to the parent. 240 // TODO(dmazzoni): see if this can be removed after 241 // https://bugs.webkit.org/show_bug.cgi?id=68466 is fixed. 242 if (obj.axID() != root_id) { 243 WebAXObject parent = obj.parentObject(); 244 while (!parent.isDetached() && 245 parent.accessibilityIsIgnored()) { 246 parent = parent.parentObject(); 247 } 248 249 if (parent.isDetached()) { 250 NOTREACHED(); 251 continue; 252 } 253 bool is_child_of_parent = false; 254 for (unsigned int i = 0; i < parent.childCount(); ++i) { 255 if (parent.childAt(i).equals(obj)) { 256 is_child_of_parent = true; 257 break; 258 } 259 } 260 261 if (!is_child_of_parent) { 262 obj = parent; 263 event.id = obj.axID(); 264 } 265 } 266 267 // Allow WebKit to cache intermediate results since we're doing a bunch 268 // of read-only queries at once. 269 root_object.startCachingComputedObjectAttributesUntilTreeMutates(); 270 271 AccessibilityHostMsg_EventParams event_msg; 272 event_msg.event_type = event.event_type; 273 event_msg.id = event.id; 274 std::set<int> ids_serialized; 275 SerializeChangedNodes(obj, &event_msg.nodes, &ids_serialized); 276 event_msgs.push_back(event_msg); 277 278 #ifndef NDEBUG 279 if (logging_) { 280 AccessibilityNodeDataTreeNode tree; 281 MakeAccessibilityNodeDataTree(event_msg.nodes, &tree); 282 VLOG(0) << "Accessibility update: \n" 283 << "routing id=" << routing_id() 284 << " event=" 285 << AccessibilityEventToString(event.event_type) 286 << "\n" << tree.DebugString(true); 287 } 288 #endif 289 } 290 291 AppendLocationChangeEvents(&event_msgs); 292 293 Send(new AccessibilityHostMsg_Events(routing_id(), event_msgs)); 294 } 295 296 void RendererAccessibilityComplete::AppendLocationChangeEvents( 297 std::vector<AccessibilityHostMsg_EventParams>* event_msgs) { 298 std::queue<WebAXObject> objs_to_explore; 299 std::vector<BrowserTreeNode*> location_changes; 300 WebAXObject root_object = GetMainDocument().accessibilityObject(); 301 objs_to_explore.push(root_object); 302 303 while (objs_to_explore.size()) { 304 WebAXObject obj = objs_to_explore.front(); 305 objs_to_explore.pop(); 306 int id = obj.axID(); 307 if (browser_id_map_.find(id) != browser_id_map_.end()) { 308 BrowserTreeNode* browser_node = browser_id_map_[id]; 309 gfx::Rect new_location = obj.boundingBoxRect(); 310 if (browser_node->location != new_location) { 311 browser_node->location = new_location; 312 location_changes.push_back(browser_node); 313 } 314 } 315 316 for (unsigned i = 0; i < obj.childCount(); ++i) 317 objs_to_explore.push(obj.childAt(i)); 318 } 319 320 if (location_changes.size() == 0) 321 return; 322 323 AccessibilityHostMsg_EventParams event_msg; 324 event_msg.event_type = static_cast<blink::WebAXEvent>(-1); 325 event_msg.id = root_object.axID(); 326 event_msg.nodes.resize(location_changes.size()); 327 for (size_t i = 0; i < location_changes.size(); i++) { 328 AccessibilityNodeData& serialized_node = event_msg.nodes[i]; 329 serialized_node.id = location_changes[i]->id; 330 serialized_node.location = location_changes[i]->location; 331 serialized_node.AddBoolAttribute( 332 AccessibilityNodeData::ATTR_UPDATE_LOCATION_ONLY, true); 333 } 334 335 event_msgs->push_back(event_msg); 336 } 337 338 RendererAccessibilityComplete::BrowserTreeNode* 339 RendererAccessibilityComplete::CreateBrowserTreeNode() { 340 return new RendererAccessibilityComplete::BrowserTreeNode(); 341 } 342 343 void RendererAccessibilityComplete::SerializeChangedNodes( 344 const blink::WebAXObject& obj, 345 std::vector<AccessibilityNodeData>* dst, 346 std::set<int>* ids_serialized) { 347 if (ids_serialized->find(obj.axID()) != ids_serialized->end()) 348 return; 349 ids_serialized->insert(obj.axID()); 350 351 // This method has three responsibilities: 352 // 1. Serialize |obj| into an AccessibilityNodeData, and append it to 353 // the end of the |dst| vector to be send to the browser process. 354 // 2. Determine if |obj| has any new children that the browser doesn't 355 // know about yet, and call SerializeChangedNodes recursively on those. 356 // 3. Update our internal data structure that keeps track of what nodes 357 // the browser knows about. 358 359 // First, find the BrowserTreeNode for this id in our data structure where 360 // we keep track of what accessibility objects the browser already knows 361 // about. If we don't find it, then this must be the new root of the 362 // accessibility tree. 363 BrowserTreeNode* browser_node = NULL; 364 base::hash_map<int32, BrowserTreeNode*>::iterator iter = 365 browser_id_map_.find(obj.axID()); 366 if (iter != browser_id_map_.end()) { 367 browser_node = iter->second; 368 } else { 369 if (browser_root_) { 370 ClearBrowserTreeNode(browser_root_); 371 browser_id_map_.erase(browser_root_->id); 372 delete browser_root_; 373 } 374 browser_root_ = CreateBrowserTreeNode(); 375 browser_node = browser_root_; 376 browser_node->id = obj.axID(); 377 browser_node->location = obj.boundingBoxRect(); 378 browser_node->parent = NULL; 379 browser_id_map_[browser_node->id] = browser_node; 380 } 381 382 // Iterate over the ids of the children of |obj|. 383 // Create a set of the child ids so we can quickly look 384 // up which children are new and which ones were there before. 385 // Also catch the case where a child is already in the browser tree 386 // data structure with a different parent, and make sure the old parent 387 // clears this node first. 388 base::hash_set<int32> new_child_ids; 389 const WebDocument& document = GetMainDocument(); 390 for (unsigned i = 0; i < obj.childCount(); i++) { 391 WebAXObject child = obj.childAt(i); 392 if (ShouldIncludeChildNode(obj, child)) { 393 int new_child_id = child.axID(); 394 new_child_ids.insert(new_child_id); 395 396 BrowserTreeNode* child = browser_id_map_[new_child_id]; 397 if (child && child->parent != browser_node) { 398 // The child is being reparented. Find the WebKit accessibility 399 // object corresponding to the old parent, or the closest ancestor 400 // still in the tree. 401 BrowserTreeNode* parent = child->parent; 402 WebAXObject parent_obj; 403 while (parent) { 404 parent_obj = document.accessibilityObjectFromID(parent->id); 405 406 if (!parent_obj.isDetached()) 407 break; 408 parent = parent->parent; 409 } 410 CHECK(parent); 411 // Call SerializeChangedNodes recursively on the old parent, 412 // so that the update that clears |child| from its old parent 413 // occurs stricly before the update that adds |child| to its 414 // new parent. 415 SerializeChangedNodes(parent_obj, dst, ids_serialized); 416 } 417 } 418 } 419 420 // Go through the old children and delete subtrees for child 421 // ids that are no longer present, and create a map from 422 // id to BrowserTreeNode for the rest. It's important to delete 423 // first in a separate pass so that nodes that are reparented 424 // don't end up children of two different parents in the middle 425 // of an update, which can lead to a double-free. 426 base::hash_map<int32, BrowserTreeNode*> browser_child_id_map; 427 std::vector<BrowserTreeNode*> old_children; 428 old_children.swap(browser_node->children); 429 for (size_t i = 0; i < old_children.size(); i++) { 430 BrowserTreeNode* old_child = old_children[i]; 431 int old_child_id = old_child->id; 432 if (new_child_ids.find(old_child_id) == new_child_ids.end()) { 433 browser_id_map_.erase(old_child_id); 434 ClearBrowserTreeNode(old_child); 435 delete old_child; 436 } else { 437 browser_child_id_map[old_child_id] = old_child; 438 } 439 } 440 441 // Serialize this node. This fills in all of the fields in 442 // AccessibilityNodeData except child_ids, which we handle below. 443 dst->push_back(AccessibilityNodeData()); 444 AccessibilityNodeData* serialized_node = &dst->back(); 445 SerializeAccessibilityNode(obj, serialized_node); 446 if (serialized_node->id == browser_root_->id) 447 serialized_node->role = blink::WebAXRoleRootWebArea; 448 449 // Iterate over the children, make note of the ones that are new 450 // and need to be serialized, and update the BrowserTreeNode 451 // data structure to reflect the new tree. 452 std::vector<WebAXObject> children_to_serialize; 453 int child_count = obj.childCount(); 454 browser_node->children.reserve(child_count); 455 for (int i = 0; i < child_count; i++) { 456 WebAXObject child = obj.childAt(i); 457 int child_id = child.axID(); 458 459 // Checks to make sure the child is valid, attached to this node, 460 // and one we want to include in the tree. 461 if (!ShouldIncludeChildNode(obj, child)) 462 continue; 463 464 // No need to do anything more with children that aren't new; 465 // the browser will reuse its existing object. 466 if (new_child_ids.find(child_id) == new_child_ids.end()) 467 continue; 468 469 new_child_ids.erase(child_id); 470 serialized_node->child_ids.push_back(child_id); 471 if (browser_child_id_map.find(child_id) != browser_child_id_map.end()) { 472 BrowserTreeNode* reused_child = browser_child_id_map[child_id]; 473 browser_node->children.push_back(reused_child); 474 } else { 475 BrowserTreeNode* new_child = CreateBrowserTreeNode(); 476 new_child->id = child_id; 477 new_child->location = obj.boundingBoxRect(); 478 new_child->parent = browser_node; 479 browser_node->children.push_back(new_child); 480 browser_id_map_[child_id] = new_child; 481 children_to_serialize.push_back(child); 482 } 483 } 484 485 // Serialize all of the new children, recursively. 486 for (size_t i = 0; i < children_to_serialize.size(); ++i) 487 SerializeChangedNodes(children_to_serialize[i], dst, ids_serialized); 488 } 489 490 void RendererAccessibilityComplete::ClearBrowserTreeNode( 491 BrowserTreeNode* browser_node) { 492 for (size_t i = 0; i < browser_node->children.size(); ++i) { 493 browser_id_map_.erase(browser_node->children[i]->id); 494 ClearBrowserTreeNode(browser_node->children[i]); 495 delete browser_node->children[i]; 496 } 497 browser_node->children.clear(); 498 } 499 500 void RendererAccessibilityComplete::OnDoDefaultAction(int acc_obj_id) { 501 const WebDocument& document = GetMainDocument(); 502 if (document.isNull()) 503 return; 504 505 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 506 if (obj.isDetached()) { 507 #ifndef NDEBUG 508 if (logging_) 509 LOG(WARNING) << "DoDefaultAction on invalid object id " << acc_obj_id; 510 #endif 511 return; 512 } 513 514 obj.performDefaultAction(); 515 } 516 517 void RendererAccessibilityComplete::OnScrollToMakeVisible( 518 int acc_obj_id, gfx::Rect subfocus) { 519 const WebDocument& document = GetMainDocument(); 520 if (document.isNull()) 521 return; 522 523 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 524 if (obj.isDetached()) { 525 #ifndef NDEBUG 526 if (logging_) 527 LOG(WARNING) << "ScrollToMakeVisible on invalid object id " << acc_obj_id; 528 #endif 529 return; 530 } 531 532 obj.scrollToMakeVisibleWithSubFocus( 533 WebRect(subfocus.x(), subfocus.y(), 534 subfocus.width(), subfocus.height())); 535 536 // Make sure the browser gets an event when the scroll 537 // position actually changes. 538 // TODO(dmazzoni): remove this once this bug is fixed: 539 // https://bugs.webkit.org/show_bug.cgi?id=73460 540 HandleWebAccessibilityEvent( 541 document.accessibilityObject(), 542 blink::WebAXEventLayoutComplete); 543 } 544 545 void RendererAccessibilityComplete::OnScrollToPoint( 546 int acc_obj_id, gfx::Point point) { 547 const WebDocument& document = GetMainDocument(); 548 if (document.isNull()) 549 return; 550 551 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 552 if (obj.isDetached()) { 553 #ifndef NDEBUG 554 if (logging_) 555 LOG(WARNING) << "ScrollToPoint on invalid object id " << acc_obj_id; 556 #endif 557 return; 558 } 559 560 obj.scrollToGlobalPoint(WebPoint(point.x(), point.y())); 561 562 // Make sure the browser gets an event when the scroll 563 // position actually changes. 564 // TODO(dmazzoni): remove this once this bug is fixed: 565 // https://bugs.webkit.org/show_bug.cgi?id=73460 566 HandleWebAccessibilityEvent( 567 document.accessibilityObject(), 568 blink::WebAXEventLayoutComplete); 569 } 570 571 void RendererAccessibilityComplete::OnSetTextSelection( 572 int acc_obj_id, int start_offset, int end_offset) { 573 const WebDocument& document = GetMainDocument(); 574 if (document.isNull()) 575 return; 576 577 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 578 if (obj.isDetached()) { 579 #ifndef NDEBUG 580 if (logging_) 581 LOG(WARNING) << "SetTextSelection on invalid object id " << acc_obj_id; 582 #endif 583 return; 584 } 585 586 // TODO(dmazzoni): support elements other than <input>. 587 blink::WebNode node = obj.node(); 588 if (!node.isNull() && node.isElementNode()) { 589 blink::WebElement element = node.to<blink::WebElement>(); 590 blink::WebInputElement* input_element = 591 blink::toWebInputElement(&element); 592 if (input_element && input_element->isTextField()) 593 input_element->setSelectionRange(start_offset, end_offset); 594 } 595 } 596 597 void RendererAccessibilityComplete::OnEventsAck() { 598 DCHECK(ack_pending_); 599 ack_pending_ = false; 600 SendPendingAccessibilityEvents(); 601 } 602 603 void RendererAccessibilityComplete::OnSetFocus(int acc_obj_id) { 604 const WebDocument& document = GetMainDocument(); 605 if (document.isNull()) 606 return; 607 608 WebAXObject obj = document.accessibilityObjectFromID(acc_obj_id); 609 if (obj.isDetached()) { 610 #ifndef NDEBUG 611 if (logging_) { 612 LOG(WARNING) << "OnSetAccessibilityFocus on invalid object id " 613 << acc_obj_id; 614 } 615 #endif 616 return; 617 } 618 619 WebAXObject root = document.accessibilityObject(); 620 if (root.isDetached()) { 621 #ifndef NDEBUG 622 if (logging_) { 623 LOG(WARNING) << "OnSetAccessibilityFocus but root is invalid"; 624 } 625 #endif 626 return; 627 } 628 629 // By convention, calling SetFocus on the root of the tree should clear the 630 // current focus. Otherwise set the focus to the new node. 631 if (acc_obj_id == root.axID()) 632 render_view()->GetWebView()->clearFocusedNode(); 633 else 634 obj.setFocused(true); 635 } 636 637 void RendererAccessibilityComplete::OnFatalError() { 638 CHECK(false) << "Invalid accessibility tree."; 639 } 640 641 } // namespace content 642