1 /*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 1999 Antti Koivisto (koivisto@kde.org)
4 * (C) 2001 Dirk Mueller (mueller@kde.org)
5 * (C) 2006 Alexey Proskuryakov (ap@webkit.org)
6 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All rights reserved.
7 * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/)
8 * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies)
9 * Copyright (C) 2013 Google Inc. All rights reserved.
10 *
11 * This library is free software; you can redistribute it and/or
12 * modify it under the terms of the GNU Library General Public
13 * License as published by the Free Software Foundation; either
14 * version 2 of the License, or (at your option) any later version.
15 *
16 * This library is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 * Library General Public License for more details.
20 *
21 * You should have received a copy of the GNU Library General Public License
22 * along with this library; see the file COPYING.LIB. If not, write to
23 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 * Boston, MA 02110-1301, USA.
25 *
26 */
27
28 #include "config.h"
29 #include "core/dom/Fullscreen.h"
30
31 #include "core/HTMLNames.h"
32 #include "core/dom/Document.h"
33 #include "core/events/Event.h"
34 #include "core/frame/FrameHost.h"
35 #include "core/frame/LocalFrame.h"
36 #include "core/frame/Settings.h"
37 #include "core/frame/UseCounter.h"
38 #include "core/html/HTMLIFrameElement.h"
39 #include "core/html/HTMLMediaElement.h"
40 #include "core/page/Chrome.h"
41 #include "core/page/ChromeClient.h"
42 #include "core/page/EventHandler.h"
43 #include "core/rendering/RenderFullScreen.h"
44 #include "platform/UserGestureIndicator.h"
45
46 namespace blink {
47
48 using namespace HTMLNames;
49
fullscreenIsAllowedForAllOwners(const Document & document)50 static bool fullscreenIsAllowedForAllOwners(const Document& document)
51 {
52 for (const Element* owner = document.ownerElement(); owner; owner = owner->document().ownerElement()) {
53 if (!isHTMLIFrameElement(owner))
54 return false;
55 if (!owner->hasAttribute(allowfullscreenAttr))
56 return false;
57 }
58 return true;
59 }
60
fullscreenIsSupported(const Document & document)61 static bool fullscreenIsSupported(const Document& document)
62 {
63 // Fullscreen is supported if there is no previously-established user preference,
64 // security risk, or platform limitation.
65 return !document.settings() || document.settings()->fullscreenSupported();
66 }
67
fullscreenIsSupported(const Document & document,const Element & element)68 static bool fullscreenIsSupported(const Document& document, const Element& element)
69 {
70 if (!document.settings() || (document.settings()->disallowFullscreenForNonMediaElements() && !isHTMLMediaElement(element)))
71 return false;
72 return fullscreenIsSupported(document);
73 }
74
fullscreenElementReady(const Element & element,Fullscreen::RequestType requestType)75 static bool fullscreenElementReady(const Element& element, Fullscreen::RequestType requestType)
76 {
77 // A fullscreen element ready check for an element |element| returns true if all of the
78 // following are true, and false otherwise:
79
80 // |element| is in a document.
81 if (!element.inDocument())
82 return false;
83
84 // |element|'s node document's fullscreen enabled flag is set.
85 if (!fullscreenIsAllowedForAllOwners(element.document())) {
86 if (requestType == Fullscreen::PrefixedVideoRequest)
87 UseCounter::count(element.document(), UseCounter::VideoFullscreenAllowedExemption);
88 else
89 return false;
90 }
91
92 // |element|'s node document's fullscreen element stack is either empty or its top element is an
93 // inclusive ancestor of |element|.
94 if (const Element* topElement = Fullscreen::fullscreenElementFrom(element.document())) {
95 if (!topElement->contains(&element))
96 return false;
97 }
98
99 // |element| has no ancestor element whose local name is iframe and namespace is the HTML
100 // namespace.
101 if (Traversal<HTMLIFrameElement>::firstAncestor(element))
102 return false;
103
104 // |element|'s node document's browsing context either has a browsing context container and the
105 // fullscreen element ready check returns true for |element|'s node document's browsing
106 // context's browsing context container, or it has no browsing context container.
107 if (const Element* owner = element.document().ownerElement()) {
108 if (!fullscreenElementReady(*owner, requestType))
109 return false;
110 }
111
112 return true;
113 }
114
isPrefixed(const AtomicString & type)115 static bool isPrefixed(const AtomicString& type)
116 {
117 return type == EventTypeNames::webkitfullscreenchange || type == EventTypeNames::webkitfullscreenerror;
118 }
119
createEvent(const AtomicString & type,EventTarget & target)120 static PassRefPtrWillBeRawPtr<Event> createEvent(const AtomicString& type, EventTarget& target)
121 {
122 EventInit initializer;
123 initializer.bubbles = isPrefixed(type);
124 RefPtrWillBeRawPtr<Event> event = Event::create(type, initializer);
125 event->setTarget(&target);
126 return event;
127 }
128
supplementName()129 const char* Fullscreen::supplementName()
130 {
131 return "Fullscreen";
132 }
133
from(Document & document)134 Fullscreen& Fullscreen::from(Document& document)
135 {
136 Fullscreen* fullscreen = fromIfExists(document);
137 if (!fullscreen) {
138 fullscreen = new Fullscreen(document);
139 DocumentSupplement::provideTo(document, supplementName(), adoptPtrWillBeNoop(fullscreen));
140 }
141
142 return *fullscreen;
143 }
144
fromIfExistsSlow(Document & document)145 Fullscreen* Fullscreen::fromIfExistsSlow(Document& document)
146 {
147 return static_cast<Fullscreen*>(DocumentSupplement::from(document, supplementName()));
148 }
149
fullscreenElementFrom(Document & document)150 Element* Fullscreen::fullscreenElementFrom(Document& document)
151 {
152 if (Fullscreen* found = fromIfExists(document))
153 return found->fullscreenElement();
154 return 0;
155 }
156
currentFullScreenElementFrom(Document & document)157 Element* Fullscreen::currentFullScreenElementFrom(Document& document)
158 {
159 if (Fullscreen* found = fromIfExists(document))
160 return found->webkitCurrentFullScreenElement();
161 return 0;
162 }
163
isFullScreen(Document & document)164 bool Fullscreen::isFullScreen(Document& document)
165 {
166 return currentFullScreenElementFrom(document);
167 }
168
Fullscreen(Document & document)169 Fullscreen::Fullscreen(Document& document)
170 : DocumentLifecycleObserver(&document)
171 , m_areKeysEnabledInFullScreen(false)
172 , m_fullScreenRenderer(nullptr)
173 , m_eventQueueTimer(this, &Fullscreen::eventQueueTimerFired)
174 {
175 document.setHasFullscreenSupplement();
176 }
177
~Fullscreen()178 Fullscreen::~Fullscreen()
179 {
180 }
181
document()182 inline Document* Fullscreen::document()
183 {
184 return lifecycleContext();
185 }
186
documentWasDetached()187 void Fullscreen::documentWasDetached()
188 {
189 m_eventQueue.clear();
190
191 if (m_fullScreenRenderer)
192 m_fullScreenRenderer->destroy();
193
194 #if ENABLE(OILPAN)
195 m_fullScreenElement = nullptr;
196 m_fullScreenElementStack.clear();
197 #endif
198
199 }
200
201 #if !ENABLE(OILPAN)
documentWasDisposed()202 void Fullscreen::documentWasDisposed()
203 {
204 // NOTE: the context dispose phase is not supported in oilpan. Please
205 // consider using the detach phase instead.
206 m_fullScreenElement = nullptr;
207 m_fullScreenElementStack.clear();
208 }
209 #endif
210
requestFullscreen(Element & element,RequestType requestType)211 void Fullscreen::requestFullscreen(Element& element, RequestType requestType)
212 {
213 // Ignore this request if the document is not in a live frame.
214 if (!document()->isActive())
215 return;
216
217 // If |element| is on top of |doc|'s fullscreen element stack, terminate these substeps.
218 if (&element == fullscreenElement())
219 return;
220
221 do {
222 // 1. If any of the following conditions are true, terminate these steps and queue a task to fire
223 // an event named fullscreenerror with its bubbles attribute set to true on the context object's
224 // node document:
225
226 // The fullscreen element ready check returns false.
227 if (!fullscreenElementReady(element, requestType))
228 break;
229
230 // This algorithm is not allowed to show a pop-up:
231 // An algorithm is allowed to show a pop-up if, in the task in which the algorithm is running, either:
232 // - an activation behavior is currently being processed whose click event was trusted, or
233 // - the event listener for a trusted click event is being handled.
234 if (!UserGestureIndicator::processingUserGesture())
235 break;
236
237 // Fullscreen is not supported.
238 if (!fullscreenIsSupported(element.document(), element))
239 break;
240
241 // 2. Let doc be element's node document. (i.e. "this")
242 Document* currentDoc = document();
243
244 // 3. Let docs be all doc's ancestor browsing context's documents (if any) and doc.
245 Deque<Document*> docs;
246
247 do {
248 docs.prepend(currentDoc);
249 currentDoc = currentDoc->ownerElement() ? ¤tDoc->ownerElement()->document() : 0;
250 } while (currentDoc);
251
252 // 4. For each document in docs, run these substeps:
253 Deque<Document*>::iterator current = docs.begin(), following = docs.begin();
254
255 do {
256 ++following;
257
258 // 1. Let following document be the document after document in docs, or null if there is no
259 // such document.
260 Document* currentDoc = *current;
261 Document* followingDoc = following != docs.end() ? *following : 0;
262
263 // 2. If following document is null, push context object on document's fullscreen element
264 // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
265 // set to true on the document.
266 if (!followingDoc) {
267 from(*currentDoc).pushFullscreenElementStack(element, requestType);
268 enqueueChangeEvent(*currentDoc, requestType);
269 continue;
270 }
271
272 // 3. Otherwise, if document's fullscreen element stack is either empty or its top element
273 // is not following document's browsing context container,
274 Element* topElement = fullscreenElementFrom(*currentDoc);
275 if (!topElement || topElement != followingDoc->ownerElement()) {
276 // ...push following document's browsing context container on document's fullscreen element
277 // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
278 // set to true on document.
279 from(*currentDoc).pushFullscreenElementStack(*followingDoc->ownerElement(), requestType);
280 enqueueChangeEvent(*currentDoc, requestType);
281 continue;
282 }
283
284 // 4. Otherwise, do nothing for this document. It stays the same.
285 } while (++current != docs.end());
286
287 // 5. Return, and run the remaining steps asynchronously.
288 // 6. Optionally, perform some animation.
289 m_areKeysEnabledInFullScreen = requestType != PrefixedMozillaRequest && requestType != PrefixedVideoRequest;
290 document()->frameHost()->chrome().client().enterFullScreenForElement(&element);
291
292 // 7. Optionally, display a message indicating how the user can exit displaying the context object fullscreen.
293 return;
294 } while (0);
295
296 enqueueErrorEvent(element, requestType);
297 }
298
fullyExitFullscreen(Document & document)299 void Fullscreen::fullyExitFullscreen(Document& document)
300 {
301 // To fully exit fullscreen, run these steps:
302
303 // 1. Let |doc| be the top-level browsing context's document.
304 Document& doc = document.topDocument();
305
306 // 2. If |doc|'s fullscreen element stack is empty, terminate these steps.
307 if (!fullscreenElementFrom(doc))
308 return;
309
310 // 3. Remove elements from |doc|'s fullscreen element stack until only the top element is left.
311 size_t stackSize = from(doc).m_fullScreenElementStack.size();
312 from(doc).m_fullScreenElementStack.remove(0, stackSize - 1);
313 ASSERT(from(doc).m_fullScreenElementStack.size() == 1);
314
315 // 4. Act as if the exitFullscreen() method was invoked on |doc|.
316 from(doc).exitFullscreen();
317 }
318
exitFullscreen()319 void Fullscreen::exitFullscreen()
320 {
321 // The exitFullscreen() method must run these steps:
322
323 // 1. Let doc be the context object. (i.e. "this")
324 Document* currentDoc = document();
325 if (!currentDoc->isActive())
326 return;
327
328 // 2. If doc's fullscreen element stack is empty, terminate these steps.
329 if (m_fullScreenElementStack.isEmpty())
330 return;
331
332 // 3. Let descendants be all the doc's descendant browsing context's documents with a non-empty fullscreen
333 // element stack (if any), ordered so that the child of the doc is last and the document furthest
334 // away from the doc is first.
335 WillBeHeapDeque<RefPtrWillBeMember<Document> > descendants;
336 for (Frame* descendant = document()->frame() ? document()->frame()->tree().traverseNext() : 0; descendant; descendant = descendant->tree().traverseNext()) {
337 if (!descendant->isLocalFrame())
338 continue;
339 ASSERT(toLocalFrame(descendant)->document());
340 if (fullscreenElementFrom(*toLocalFrame(descendant)->document()))
341 descendants.prepend(toLocalFrame(descendant)->document());
342 }
343
344 // 4. For each descendant in descendants, empty descendant's fullscreen element stack, and queue a
345 // task to fire an event named fullscreenchange with its bubbles attribute set to true on descendant.
346 for (WillBeHeapDeque<RefPtrWillBeMember<Document> >::iterator i = descendants.begin(); i != descendants.end(); ++i) {
347 ASSERT(*i);
348 RequestType requestType = from(**i).m_fullScreenElementStack.last().second;
349 from(**i).clearFullscreenElementStack();
350 enqueueChangeEvent(**i, requestType);
351 }
352
353 // 5. While doc is not null, run these substeps:
354 Element* newTop = 0;
355 while (currentDoc) {
356 RequestType requestType = from(*currentDoc).m_fullScreenElementStack.last().second;
357
358 // 1. Pop the top element of doc's fullscreen element stack.
359 from(*currentDoc).popFullscreenElementStack();
360
361 // If doc's fullscreen element stack is non-empty and the element now at the top is either
362 // not in a document or its node document is not doc, repeat this substep.
363 newTop = fullscreenElementFrom(*currentDoc);
364 if (newTop && (!newTop->inDocument() || newTop->document() != currentDoc))
365 continue;
366
367 // 2. Queue a task to fire an event named fullscreenchange with its bubbles attribute set to true
368 // on doc.
369 enqueueChangeEvent(*currentDoc, requestType);
370
371 // 3. If doc's fullscreen element stack is empty and doc's browsing context has a browsing context
372 // container, set doc to that browsing context container's node document.
373 if (!newTop && currentDoc->ownerElement()) {
374 currentDoc = ¤tDoc->ownerElement()->document();
375 continue;
376 }
377
378 // 4. Otherwise, set doc to null.
379 currentDoc = 0;
380 }
381
382 // 6. Return, and run the remaining steps asynchronously.
383 // 7. Optionally, perform some animation.
384
385 FrameHost* host = document()->frameHost();
386
387 // Speculative fix for engaget.com/videos per crbug.com/336239.
388 // FIXME: This check is wrong. We ASSERT(document->isActive()) above
389 // so this should be redundant and should be removed!
390 if (!host)
391 return;
392
393 // Only exit out of full screen window mode if there are no remaining elements in the
394 // full screen stack.
395 if (!newTop) {
396 host->chrome().client().exitFullScreenForElement(m_fullScreenElement.get());
397 return;
398 }
399
400 // Otherwise, notify the chrome of the new full screen element.
401 host->chrome().client().enterFullScreenForElement(newTop);
402 }
403
fullscreenEnabled(Document & document)404 bool Fullscreen::fullscreenEnabled(Document& document)
405 {
406 // 4. The fullscreenEnabled attribute must return true if the context object has its
407 // fullscreen enabled flag set and fullscreen is supported, and false otherwise.
408
409 // Top-level browsing contexts are implied to have their allowFullScreen attribute set.
410 return fullscreenIsAllowedForAllOwners(document) && fullscreenIsSupported(document);
411 }
412
didEnterFullScreenForElement(Element * element)413 void Fullscreen::didEnterFullScreenForElement(Element* element)
414 {
415 ASSERT(element);
416 if (!document()->isActive())
417 return;
418
419 if (m_fullScreenRenderer)
420 m_fullScreenRenderer->unwrapRenderer();
421
422 m_fullScreenElement = element;
423
424 // Create a placeholder block for a the full-screen element, to keep the page from reflowing
425 // when the element is removed from the normal flow. Only do this for a RenderBox, as only
426 // a box will have a frameRect. The placeholder will be created in setFullScreenRenderer()
427 // during layout.
428 RenderObject* renderer = m_fullScreenElement->renderer();
429 bool shouldCreatePlaceholder = renderer && renderer->isBox();
430 if (shouldCreatePlaceholder) {
431 m_savedPlaceholderFrameRect = toRenderBox(renderer)->frameRect();
432 m_savedPlaceholderRenderStyle = RenderStyle::clone(renderer->style());
433 }
434
435 if (m_fullScreenElement != document()->documentElement())
436 RenderFullScreen::wrapRenderer(renderer, renderer ? renderer->parent() : 0, document());
437
438 m_fullScreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(true);
439
440 // FIXME: This should not call updateStyleIfNeeded.
441 document()->setNeedsStyleRecalc(SubtreeStyleChange);
442 document()->updateRenderTreeIfNeeded();
443
444 m_fullScreenElement->didBecomeFullscreenElement();
445
446 if (document()->frame())
447 document()->frame()->eventHandler().scheduleHoverStateUpdate();
448
449 m_eventQueueTimer.startOneShot(0, FROM_HERE);
450 }
451
didExitFullScreenForElement(Element *)452 void Fullscreen::didExitFullScreenForElement(Element*)
453 {
454 if (!m_fullScreenElement)
455 return;
456
457 if (!document()->isActive())
458 return;
459
460 m_fullScreenElement->willStopBeingFullscreenElement();
461
462 m_fullScreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
463
464 m_areKeysEnabledInFullScreen = false;
465
466 if (m_fullScreenRenderer)
467 m_fullScreenRenderer->unwrapRenderer();
468
469 m_fullScreenElement = nullptr;
470 document()->setNeedsStyleRecalc(SubtreeStyleChange);
471
472 if (document()->frame())
473 document()->frame()->eventHandler().scheduleHoverStateUpdate();
474
475 // When fullyExitFullscreen is called, we call exitFullscreen on the topDocument(). That means
476 // that the events will be queued there. So if we have no events here, start the timer on the
477 // exiting document.
478 Document* exitingDocument = document();
479 if (m_eventQueue.isEmpty())
480 exitingDocument = &document()->topDocument();
481 ASSERT(exitingDocument);
482 from(*exitingDocument).m_eventQueueTimer.startOneShot(0, FROM_HERE);
483 }
484
setFullScreenRenderer(RenderFullScreen * renderer)485 void Fullscreen::setFullScreenRenderer(RenderFullScreen* renderer)
486 {
487 if (renderer == m_fullScreenRenderer)
488 return;
489
490 if (renderer && m_savedPlaceholderRenderStyle) {
491 renderer->createPlaceholder(m_savedPlaceholderRenderStyle.release(), m_savedPlaceholderFrameRect);
492 } else if (renderer && m_fullScreenRenderer && m_fullScreenRenderer->placeholder()) {
493 RenderBlock* placeholder = m_fullScreenRenderer->placeholder();
494 renderer->createPlaceholder(RenderStyle::clone(placeholder->style()), placeholder->frameRect());
495 }
496
497 if (m_fullScreenRenderer)
498 m_fullScreenRenderer->unwrapRenderer();
499 ASSERT(!m_fullScreenRenderer);
500
501 m_fullScreenRenderer = renderer;
502 }
503
fullScreenRendererDestroyed()504 void Fullscreen::fullScreenRendererDestroyed()
505 {
506 m_fullScreenRenderer = nullptr;
507 }
508
enqueueChangeEvent(Document & document,RequestType requestType)509 void Fullscreen::enqueueChangeEvent(Document& document, RequestType requestType)
510 {
511 RefPtrWillBeRawPtr<Event> event;
512 if (requestType == UnprefixedRequest) {
513 event = createEvent(EventTypeNames::fullscreenchange, document);
514 } else {
515 ASSERT(document.hasFullscreenSupplement());
516 Fullscreen& fullscreen = from(document);
517 EventTarget* target = fullscreen.fullscreenElement();
518 if (!target)
519 target = fullscreen.webkitCurrentFullScreenElement();
520 if (!target)
521 target = &document;
522 event = createEvent(EventTypeNames::webkitfullscreenchange, *target);
523 }
524 m_eventQueue.append(event);
525 // NOTE: The timer is started in didEnterFullScreenForElement/didExitFullScreenForElement.
526 }
527
enqueueErrorEvent(Element & element,RequestType requestType)528 void Fullscreen::enqueueErrorEvent(Element& element, RequestType requestType)
529 {
530 RefPtrWillBeRawPtr<Event> event;
531 if (requestType == UnprefixedRequest)
532 event = createEvent(EventTypeNames::fullscreenerror, element.document());
533 else
534 event = createEvent(EventTypeNames::webkitfullscreenerror, element);
535 m_eventQueue.append(event);
536 m_eventQueueTimer.startOneShot(0, FROM_HERE);
537 }
538
eventQueueTimerFired(Timer<Fullscreen> *)539 void Fullscreen::eventQueueTimerFired(Timer<Fullscreen>*)
540 {
541 // Since we dispatch events in this function, it's possible that the
542 // document will be detached and GC'd. We protect it here to make sure we
543 // can finish the function successfully.
544 RefPtrWillBeRawPtr<Document> protectDocument(document());
545 WillBeHeapDeque<RefPtrWillBeMember<Event> > eventQueue;
546 m_eventQueue.swap(eventQueue);
547
548 while (!eventQueue.isEmpty()) {
549 RefPtrWillBeRawPtr<Event> event = eventQueue.takeFirst();
550 Node* target = event->target()->toNode();
551
552 // If the element was removed from our tree, also message the documentElement.
553 if (!target->inDocument() && document()->documentElement()) {
554 ASSERT(isPrefixed(event->type()));
555 eventQueue.append(createEvent(event->type(), *document()->documentElement()));
556 }
557
558 target->dispatchEvent(event);
559 }
560 }
561
elementRemoved(Element & oldNode)562 void Fullscreen::elementRemoved(Element& oldNode)
563 {
564 // Whenever the removing steps run with an |oldNode| and |oldNode| is in its node document's
565 // fullscreen element stack, run these steps:
566
567 // 1. If |oldNode| is at the top of its node document's fullscreen element stack, act as if the
568 // exitFullscreen() method was invoked on that document.
569 if (fullscreenElement() == &oldNode) {
570 exitFullscreen();
571 return;
572 }
573
574 // 2. Otherwise, remove |oldNode| from its node document's fullscreen element stack.
575 for (size_t i = 0; i < m_fullScreenElementStack.size(); ++i) {
576 if (m_fullScreenElementStack[i].first.get() == &oldNode) {
577 m_fullScreenElementStack.remove(i);
578 return;
579 }
580 }
581
582 // NOTE: |oldNode| was not in the fullscreen element stack.
583 }
584
clearFullscreenElementStack()585 void Fullscreen::clearFullscreenElementStack()
586 {
587 m_fullScreenElementStack.clear();
588 }
589
popFullscreenElementStack()590 void Fullscreen::popFullscreenElementStack()
591 {
592 if (m_fullScreenElementStack.isEmpty())
593 return;
594
595 m_fullScreenElementStack.removeLast();
596 }
597
pushFullscreenElementStack(Element & element,RequestType requestType)598 void Fullscreen::pushFullscreenElementStack(Element& element, RequestType requestType)
599 {
600 m_fullScreenElementStack.append(std::make_pair(&element, requestType));
601 }
602
trace(Visitor * visitor)603 void Fullscreen::trace(Visitor* visitor)
604 {
605 visitor->trace(m_fullScreenElement);
606 visitor->trace(m_fullScreenElementStack);
607 visitor->trace(m_fullScreenRenderer);
608 visitor->trace(m_eventQueue);
609 DocumentSupplement::trace(visitor);
610 }
611
612 } // namespace blink
613