Chromium Code Reviews| Index: third_party/WebKit/Source/core/html/HTMLMarqueeElement.cpp |
| diff --git a/third_party/WebKit/Source/core/html/HTMLMarqueeElement.cpp b/third_party/WebKit/Source/core/html/HTMLMarqueeElement.cpp |
| index 1da81e76d6b184d3c023742cc8a3cde9eea043b3..da35d765e554d79963767d9d48a19934fc815f33 100644 |
| --- a/third_party/WebKit/Source/core/html/HTMLMarqueeElement.cpp |
| +++ b/third_party/WebKit/Source/core/html/HTMLMarqueeElement.cpp |
| @@ -22,32 +22,65 @@ |
| #include "core/html/HTMLMarqueeElement.h" |
| -#include "bindings/core/v8/PrivateScriptRunner.h" |
| +#include "bindings/core/v8/ExceptionStatePlaceholder.h" |
| #include "bindings/core/v8/V8HTMLMarqueeElement.h" |
| +#include "core/CSSPropertyNames.h" |
| #include "core/HTMLNames.h" |
| +#include "core/animation/DocumentTimeline.h" |
| +#include "core/animation/KeyframeEffect.h" |
| +#include "core/animation/KeyframeEffectModel.h" |
| +#include "core/animation/KeyframeEffectOptions.h" |
| +#include "core/animation/StringKeyframe.h" |
| +#include "core/animation/TimingInput.h" |
| +#include "core/css/CSSStyleDeclaration.h" |
| #include "core/dom/Document.h" |
| +#include "core/dom/shadow/ShadowRoot.h" |
| +#include "core/frame/LocalDOMWindow.h" |
| #include "core/frame/UseCounter.h" |
| -#include "platform/ScriptForbiddenScope.h" |
| +#include "core/html/HTMLDimension.h" |
| +#include <cstdlib> |
| namespace blink { |
| +namespace { |
| + |
| +String convertHTMLLengthToCSSLength(const String& htmlLength) { |
| + HTMLDimension dimension; |
| + parseDimensionValue(htmlLength, dimension); |
| + if (dimension.isRelative()) |
| + return String(); |
| + CSSPrimitiveValue* cssValue = CSSPrimitiveValue::create( |
| + dimension.value(), dimension.isPercentage() |
| + ? CSSPrimitiveValue::UnitType::Percentage |
| + : CSSPrimitiveValue::UnitType::Pixels); |
| + return cssValue->customCSSText(); |
| +} |
| + |
| +} // namespace |
| + |
| inline HTMLMarqueeElement::HTMLMarqueeElement(Document& document) |
| : HTMLElement(HTMLNames::marqueeTag, document) { |
| - if (document.contextDocument()) { |
| - ScriptForbiddenScope::AllowUserAgentScript script; |
| - v8::Local<v8::Value> classObject = |
| - PrivateScriptRunner::installClassIfNeeded(&document, |
| - "HTMLMarqueeElement"); |
| - RELEASE_ASSERT(!classObject.IsEmpty()); |
| - } |
| UseCounter::count(document, UseCounter::HTMLMarqueeElement); |
| + ShadowRoot* shadow = |
| + createShadowRootInternal(ShadowRootType::V0, ASSERT_NO_EXCEPTION); |
|
jbroman
2016/12/01 22:22:52
Ultimately this should probably be a user agent sh
adithyas
2016/12/02 19:29:49
Yup, I'd prefer to do this is in a follow-up, espe
|
| + Element* style = document.createElement("style"); |
| + style->setTextContent( |
| + ":host { display: inline-block; width: -webkit-fill-available; overflow: " |
| + "hidden; text-align: initial; white-space: nowrap; }" |
| + ":host([direction=\"up\"]), :host([direction=\"down\"]) { overflow: " |
| + "initial; overflow-y: hidden; white-space: initial; }" |
| + ":host > div { will-change: transform; }"); |
|
adithyas
2016/12/01 20:30:48
"will-change: transform" is added in to force an a
jbroman
2016/12/01 22:22:52
To elaborate slightly, this layer gets created any
|
| + shadow->appendChild(style); |
| + |
| + Element* mover = document.createElement("div"); |
| + shadow->appendChild(mover); |
| + |
| + mover->appendChild(document.createElement("content")); |
| + m_mover = mover; |
| } |
| HTMLMarqueeElement* HTMLMarqueeElement::create(Document& document) { |
| - HTMLMarqueeElement* marqueeElement = new HTMLMarqueeElement(document); |
| - V8HTMLMarqueeElement::PrivateScript::createdCallbackMethod(document.frame(), |
| - marqueeElement); |
| - return marqueeElement; |
| + return new HTMLMarqueeElement(document); |
| } |
| void HTMLMarqueeElement::attributeChanged(const QualifiedName& name, |
| @@ -55,16 +88,22 @@ void HTMLMarqueeElement::attributeChanged(const QualifiedName& name, |
| const AtomicString& newValue, |
| AttributeModificationReason reason) { |
| HTMLElement::attributeChanged(name, oldValue, newValue, reason); |
| - V8HTMLMarqueeElement::PrivateScript::attributeChangedCallbackMethod( |
| - document().frame(), this, name.toString(), oldValue, newValue); |
| + attributeChangedCallback(name, newValue); |
| } |
| Node::InsertionNotificationRequest HTMLMarqueeElement::insertedInto( |
| ContainerNode* insertionPoint) { |
| HTMLElement::insertedInto(insertionPoint); |
| + |
| if (isConnected()) { |
| - V8HTMLMarqueeElement::PrivateScript::attachedCallbackMethod( |
| - document().frame(), this); |
| + std::vector<QualifiedName> presentationalAttributes = { |
|
jbroman
2016/12/01 22:22:52
as a rule we prefer WTF::Vector to std::vector ins
adithyas
2016/12/02 19:29:49
Fixed.
|
| + HTMLNames::bgcolorAttr, HTMLNames::heightAttr, HTMLNames::hspaceAttr, |
| + HTMLNames::vspaceAttr, HTMLNames::widthAttr}; |
| + for (auto attr : presentationalAttributes) { |
| + initializeAttribute(attr); |
| + } |
| + |
| + this->start(); |
|
jbroman
2016/12/01 22:22:52
nit: in C++, prefer "start();" to "this->start();"
adithyas
2016/12/02 19:29:49
Fixed, here and everywhere else.
|
| } |
| return InsertionDone; |
| } |
| @@ -72,14 +111,413 @@ Node::InsertionNotificationRequest HTMLMarqueeElement::insertedInto( |
| void HTMLMarqueeElement::removedFrom(ContainerNode* insertionPoint) { |
| HTMLElement::removedFrom(insertionPoint); |
| if (insertionPoint->isConnected()) { |
| - V8HTMLMarqueeElement::PrivateScript::detachedCallbackMethod( |
| - insertionPoint->document().frame(), this); |
| + this->stop(); |
| } |
| } |
| bool HTMLMarqueeElement::isHorizontal() const { |
| - AtomicString direction = getAttribute(HTMLNames::directionAttr); |
| - return direction != "down" && direction != "up"; |
| + Direction direction = this->direction(); |
| + return direction != Up && direction != Down; |
| +} |
| + |
| +int HTMLMarqueeElement::scrollAmount() { |
| + bool ok; |
| + int scrollAmount = getAttribute(HTMLNames::scrollamountAttr).toInt(&ok); |
| + if (!ok || scrollAmount < 0) { |
|
jbroman
2016/12/01 22:22:52
not-really-a-nit: Here and elsewhere, you're free
adithyas
2016/12/02 19:29:49
Removed the braces everywhere I didn't need them.
|
| + return kDefaultScrollAmount; |
| + } |
| + return scrollAmount; |
| +} |
| + |
| +void HTMLMarqueeElement::setScrollAmount(int value, |
| + ExceptionState& exceptionState) { |
| + if (value < 0) { |
| + exceptionState.throwDOMException( |
|
jbroman
2016/12/01 22:22:52
Here and elsewhere: ExceptionState::throwDOMExcept
adithyas
2016/12/02 19:29:49
Forgot about that! Fixed.
|
| + IndexSizeError, |
| + "The provided value (" + String::number(value) + ") is negative."); |
| + } |
| + setIntegralAttribute(HTMLNames::scrollamountAttr, value); |
| +} |
| + |
| +int HTMLMarqueeElement::scrollDelay() { |
| + bool ok; |
| + int scrollDelay = getAttribute(HTMLNames::scrolldelayAttr).toInt(&ok); |
| + if (!ok || scrollDelay < 0) { |
| + return kDefaultScrollDelayMS; |
| + } |
| + return scrollDelay; |
| +} |
| + |
| +void HTMLMarqueeElement::setScrollDelay(int value, |
| + ExceptionState& exceptionState) { |
| + if (value < 0) { |
| + exceptionState.throwDOMException( |
| + IndexSizeError, |
| + "The provided value (" + String::number(value) + ") is negative."); |
| + } |
| + setIntegralAttribute(HTMLNames::scrolldelayAttr, value); |
| +} |
| + |
| +int HTMLMarqueeElement::loop() { |
| + bool ok; |
| + int loop = getAttribute(HTMLNames::loopAttr).toInt(&ok); |
| + if (!ok || loop <= 0) { |
| + return kDefaultLoopLimit; |
| + } |
| + return loop; |
| +} |
| + |
| +void HTMLMarqueeElement::setLoop(int value, ExceptionState& exceptionState) { |
| + if (value <= 0 && value != -1) { |
| + exceptionState.throwDOMException( |
| + IndexSizeError, "The provided value (" + String::number(value) + |
| + ") is neither positive nor -1."); |
| + } |
| + setIntegralAttribute(HTMLNames::loopAttr, value); |
| +} |
| + |
| +void HTMLMarqueeElement::start() { |
| + // User script must not run in a SVGImage, but it's okay to run user |
| + // agent's script such as <marquee>. However, a function scheduled with |
| + // requestAnimationFrame is indistinguishable if it's scheduled by user |
| + // script or user agent's script. Thus we disallow scheduling a task |
| + // in svg (not limited to SVGImage) entirely. |
| + if (document().hasSVGRootNode()) |
|
jbroman
2016/12/01 22:22:52
I wonder if this still applies. Keeping it for now
adithyas
2016/12/02 19:29:49
Removed it, it's not necessary anymore.
|
| + return; |
| + |
| + if (m_continueCallbackRequestId) { |
| + return; |
| + } |
| + RequestAnimationFrameCallback* callback = |
| + new RequestAnimationFrameCallback(this); |
| + m_continueCallbackRequestId = document().requestAnimationFrame(callback); |
| +} |
| + |
| +void HTMLMarqueeElement::stop() { |
| + if (m_continueCallbackRequestId) { |
| + document().cancelAnimationFrame(m_continueCallbackRequestId); |
| + m_continueCallbackRequestId = 0; |
| + return; |
| + } |
| + |
| + if (m_player) { |
| + m_player->pause(); |
| + } |
| +} |
| + |
| +void HTMLMarqueeElement::initializeAttribute(const QualifiedName& attr) { |
| + const AtomicString& value = getAttribute(attr); |
| + if (value.isNull()) |
| + return; |
| + attributeChangedCallback(attr, value); |
| +} |
| + |
| +void HTMLMarqueeElement::attributeChangedCallback(const QualifiedName& attr, |
| + const String& newValue) { |
| + if (attr == HTMLNames::bgcolorAttr) { |
| + style()->setProperty("bgcolor", newValue, String(), ASSERT_NO_EXCEPTION); |
|
jbroman
2016/12/01 22:22:52
The CSS property is background-color, not bgcolor.
adithyas
2016/12/02 19:29:49
You are correct, they do use the hyphenated names,
|
| + } else if (attr == HTMLNames::heightAttr) { |
| + style()->setProperty("height", convertHTMLLengthToCSSLength(newValue), |
| + String(), ASSERT_NO_EXCEPTION); |
| + } else if (attr == HTMLNames::hspaceAttr) { |
| + style()->setProperty("marginLeft", convertHTMLLengthToCSSLength(newValue), |
| + String(), ASSERT_NO_EXCEPTION); |
| + style()->setProperty("marginRight", convertHTMLLengthToCSSLength(newValue), |
| + String(), ASSERT_NO_EXCEPTION); |
| + } else if (attr == HTMLNames::vspaceAttr) { |
| + style()->setProperty("marginTop", convertHTMLLengthToCSSLength(newValue), |
| + String(), ASSERT_NO_EXCEPTION); |
| + style()->setProperty("marginBottom", convertHTMLLengthToCSSLength(newValue), |
| + String(), ASSERT_NO_EXCEPTION); |
| + } else if (attr == HTMLNames::widthAttr) { |
| + style()->setProperty("width", convertHTMLLengthToCSSLength(newValue), |
| + String(), ASSERT_NO_EXCEPTION); |
| + } |
| +} |
| + |
| +void HTMLMarqueeElement::RequestAnimationFrameCallback::handleEvent(double) { |
| + m_marquee->m_continueCallbackRequestId = 0; |
| + m_marquee->continueAnimation(); |
| +} |
| + |
| +void HTMLMarqueeElement::AnimationFinished::handleEvent( |
| + ExecutionContext* context, |
| + Event* event) { |
| + ++m_marquee->m_loopCount; |
| + m_marquee->start(); |
| +} |
| + |
| +StringKeyframeEffectModel* HTMLMarqueeElement::createEffectModel( |
| + AnimationParameters& parameters) { |
| + StringKeyframeVector keyframes; |
| + StyleSheetContents* styleSheetContents = |
| + m_mover->document().elementSheet().contents(); |
| + |
| + RefPtr<StringKeyframe> keyframe1 = StringKeyframe::create(); |
| + keyframe1->setCSSPropertyValue(CSSPropertyTransform, |
| + parameters.transformBegin, styleSheetContents); |
| + keyframes.append(keyframe1); |
|
jbroman
2016/12/01 22:22:52
nit: when passing ownership of a RefPtr, prefer to
adithyas
2016/12/02 19:29:49
Fixed! I'm going to keep using StringKeyFrame unle
|
| + |
| + RefPtr<StringKeyframe> keyframe2 = StringKeyframe::create(); |
| + keyframe2->setCSSPropertyValue(CSSPropertyTransform, parameters.transformEnd, |
| + styleSheetContents); |
| + keyframes.append(keyframe2); |
| + |
| + StringKeyframeEffectModel* effectModel = StringKeyframeEffectModel::create( |
| + keyframes, LinearTimingFunction::shared()); |
| + return effectModel; |
|
jbroman
2016/12/01 22:22:52
super-nit: might as well just
return StringKeyfra
adithyas
2016/12/02 19:29:49
Fixed.
|
| +} |
| + |
| +// static |
| +void HTMLMarqueeElement::initializeTiming(Timing& timing, double duration) { |
|
jbroman
2016/12/01 22:22:51
You can just return Timing from this method. Or be
adithyas
2016/12/02 19:29:49
removed the method
|
| + TimingInput::setFillMode(timing, "forwards"); |
|
jbroman
2016/12/01 22:22:52
I'd just use the enum etc here instead of converti
adithyas
2016/12/02 19:29:49
I've changed it to use the enum directly for fill
jbroman
2016/12/03 23:47:31
Ah, OK.
|
| + UnrestrictedDoubleOrString iterDuration; |
| + iterDuration.setUnrestrictedDouble(duration); |
| + TimingInput::setIterationDuration(timing, iterDuration, ASSERT_NO_EXCEPTION); |
| +} |
| + |
| +void HTMLMarqueeElement::continueAnimation() { |
| + if (!shouldContinue()) { |
| + return; |
| + } |
| + if (m_player && m_player->playState() == "paused") { |
| + m_player->play(); |
| + return; |
| + } |
| + |
| + AnimationParameters parameters = getAnimationParameters(); |
| + int scrollDelay = this->scrollDelay(); |
| + int scrollAmount = this->scrollAmount(); |
| + |
| + if (scrollDelay < kMinimumScrollDelayMS && !trueSpeed()) |
| + scrollDelay = kDefaultScrollDelayMS; |
| + double duration = 0; |
| + if (scrollAmount) |
| + duration = parameters.distance * scrollDelay / scrollAmount; |
| + if (!duration) |
| + return; |
| + |
| + StringKeyframeEffectModel* effectModel = createEffectModel(parameters); |
| + Timing timing; |
| + initializeTiming(timing, duration); |
| + KeyframeEffect* keyframeEffect = |
| + KeyframeEffect::create(m_mover, effectModel, timing); |
| + Animation* player = m_mover->document().timeline().play(keyframeEffect); |
| + player->setId(""); |
|
jbroman
2016/12/01 22:22:52
nit: "player->setId(emptyString);" is slightly mor
adithyas
2016/12/02 19:29:49
Fixed (assuming you meant emptyString())
jbroman
2016/12/03 23:47:31
Indeed.
|
| + player->setOnfinish(new AnimationFinished(this)); |
| + |
| + m_player = player; |
| +} |
| + |
| +bool HTMLMarqueeElement::shouldContinue() { |
| + int lp = loop(); |
| + |
| + // By default, slide loops only once. |
| + if (lp <= 0 && behavior() == Slide) |
| + lp = 1; |
| + |
| + if (lp <= 0) |
| + return true; |
| + return m_loopCount < lp; |
| +} |
| + |
| +HTMLMarqueeElement::Behavior HTMLMarqueeElement::behavior() const { |
| + const AtomicString& bhvr = getAttribute(HTMLNames::behaviorAttr); |
| + if (bhvr == "alternate") |
| + return Alternate; |
| + if (bhvr == "slide") |
| + return Slide; |
| + return Scroll; |
| +} |
| + |
| +HTMLMarqueeElement::Direction HTMLMarqueeElement::direction() const { |
| + const AtomicString& dir = getAttribute(HTMLNames::directionAttr); |
| + if (dir == "down") |
| + return Down; |
| + if (dir == "up") |
| + return Up; |
| + if (dir == "right") |
| + return Right; |
| + return Left; |
| +} |
| + |
| +bool HTMLMarqueeElement::trueSpeed() const { |
| + return hasAttribute(HTMLNames::truespeedAttr); |
| +} |
| + |
| +HTMLMarqueeElement::Metrics HTMLMarqueeElement::getMetrics() { |
| + Metrics metrics; |
| + CSSStyleDeclaration* marqueeStyle = |
| + document().domWindow()->getComputedStyle(this, String()); |
| + |
| + // For marquees that are declared inline, getComputedStyle returns "auto" for |
| + // width and height. Setting all the metrics to zero disables animation for |
| + // inline marquees. |
| + if (marqueeStyle->getPropertyValue("width") == "auto" && |
| + marqueeStyle->getPropertyValue("height") == "auto") { |
| + metrics.contentHeight = 0; |
| + metrics.contentWidth = 0; |
| + metrics.marqueeWidth = 0; |
| + metrics.marqueeHeight = 0; |
| + return metrics; |
| + } |
| + |
| + if (isHorizontal()) { |
| + m_mover->style()->setProperty("width", "-webkit-max-content", "important", |
| + ASSERT_NO_EXCEPTION); |
| + |
| + } else { |
| + m_mover->style()->setProperty("height", "-webkit-max-content", "important", |
| + ASSERT_NO_EXCEPTION); |
| + } |
| + |
| + CSSStyleDeclaration* moverStyle = |
| + document().domWindow()->getComputedStyle(m_mover, String()); |
| + |
| + metrics.contentWidth = moverStyle->getPropertyValue("width").toDouble(); |
| + metrics.contentHeight = moverStyle->getPropertyValue("height").toDouble(); |
| + metrics.marqueeWidth = marqueeStyle->getPropertyValue("width").toDouble(); |
| + metrics.marqueeHeight = marqueeStyle->getPropertyValue("height").toDouble(); |
| + |
| + if (isHorizontal()) { |
| + m_mover->style()->setProperty("width", "", "important", |
| + ASSERT_NO_EXCEPTION); |
| + } else { |
| + m_mover->style()->setProperty("height", "", "important", |
| + ASSERT_NO_EXCEPTION); |
| + } |
| + |
| + return metrics; |
| +} |
| + |
| +HTMLMarqueeElement::AnimationParameters |
| +HTMLMarqueeElement::getAnimationParameters() { |
| + AnimationParameters parameters; |
| + Metrics metrics = getMetrics(); |
| + |
| + double totalWidth = metrics.marqueeWidth + metrics.contentWidth; |
| + double totalHeight = metrics.marqueeHeight + metrics.contentHeight; |
| + |
| + double innerWidth = metrics.marqueeWidth - metrics.contentWidth; |
| + double innerHeight = metrics.marqueeHeight - metrics.contentHeight; |
| + |
| + switch (behavior()) { |
| + case Alternate: |
| + switch (direction()) { |
| + case Right: |
| + parameters.transformBegin = |
| + createTransform(false, innerWidth >= 0 ? 0 : innerWidth); |
| + parameters.transformEnd = |
| + createTransform(false, innerWidth >= 0 ? innerWidth : 0); |
| + parameters.distance = std::abs(innerWidth); |
| + break; |
| + case Up: |
| + parameters.transformBegin = |
| + createTransform(false, innerHeight >= 0 ? innerHeight : 0); |
| + parameters.transformEnd = |
| + createTransform(false, innerHeight >= 0 ? 0 : innerHeight); |
| + parameters.distance = std::abs(innerHeight); |
| + break; |
| + case Down: |
| + parameters.transformBegin = |
| + createTransform(false, innerHeight >= 0 ? 0 : innerHeight); |
| + parameters.transformEnd = |
| + createTransform(false, innerHeight >= 0 ? innerHeight : 0); |
| + parameters.distance = std::abs(innerHeight); |
| + break; |
| + case Left: |
| + default: |
| + parameters.transformBegin = |
| + createTransform(false, innerWidth >= 0 ? innerWidth : 0); |
| + parameters.transformEnd = |
| + createTransform(false, innerWidth >= 0 ? 0 : innerWidth); |
| + parameters.distance = std::abs(innerWidth); |
| + } |
| + |
| + if (m_loopCount % 2) { |
| + AtomicString transform = parameters.transformBegin; |
| + parameters.transformBegin = parameters.transformEnd; |
| + parameters.transformEnd = transform; |
| + } |
| + |
| + break; |
| + case Slide: |
| + switch (direction()) { |
| + case Right: |
| + parameters.transformBegin = |
| + createTransform(true, metrics.contentWidth); |
| + parameters.transformEnd = createTransform(false, innerWidth); |
| + parameters.distance = metrics.marqueeWidth; |
| + break; |
| + case Up: |
| + parameters.transformBegin = |
| + createTransform(false, metrics.marqueeHeight); |
| + parameters.transformEnd = "translateY(0)"; |
| + parameters.distance = metrics.marqueeHeight; |
| + break; |
| + case Down: |
| + parameters.transformBegin = |
| + createTransform(true, metrics.contentHeight); |
| + parameters.transformEnd = createTransform(false, innerHeight); |
| + parameters.distance = metrics.marqueeHeight; |
| + break; |
| + case Left: |
| + default: |
| + parameters.transformBegin = |
| + createTransform(false, metrics.marqueeWidth); |
| + parameters.transformEnd = "translateX(0)"; |
| + parameters.distance = metrics.marqueeWidth; |
| + } |
| + break; |
| + case Scroll: |
| + default: |
| + switch (direction()) { |
| + case Right: |
| + parameters.transformBegin = |
| + createTransform(true, metrics.contentWidth); |
| + parameters.transformEnd = |
| + createTransform(false, metrics.marqueeWidth); |
| + parameters.distance = totalWidth; |
| + break; |
| + case Up: |
| + parameters.transformBegin = |
| + createTransform(false, metrics.marqueeHeight); |
| + parameters.transformEnd = |
| + createTransform(true, metrics.contentHeight); |
| + parameters.distance = totalHeight; |
| + break; |
| + case Down: |
| + parameters.transformBegin = |
| + createTransform(true, metrics.contentHeight); |
| + parameters.transformEnd = |
| + createTransform(false, metrics.marqueeHeight); |
| + parameters.distance = totalHeight; |
| + break; |
| + case Left: |
| + default: |
| + parameters.transformBegin = |
| + createTransform(false, metrics.marqueeWidth); |
| + parameters.transformEnd = createTransform(true, metrics.contentWidth); |
| + parameters.distance = totalWidth; |
| + } |
| + break; |
| + } |
| + |
| + return parameters; |
| +} |
| + |
| +AtomicString HTMLMarqueeElement::createTransform(bool isNegative, |
| + double value) const { |
| + char axis = isHorizontal() ? 'X' : 'Y'; |
| + if (isNegative) |
| + return AtomicString(String::format("translate%c(-%fpx)", axis, value)); |
| + return AtomicString(String::format("translate%c(%fpx)", axis, value)); |
| +} |
| + |
| +DEFINE_TRACE(HTMLMarqueeElement) { |
| + visitor->trace(m_mover); |
| + visitor->trace(m_player); |
| + HTMLElement::trace(visitor); |
| } |
| } // namespace blink |