Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / core / html / HTMLTextFormControlElement.cpp
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  * Copyright (C) 2004, 2005, 2006, 2007 Apple Inc. All rights reserved.
6  *           (C) 2006 Alexey Proskuryakov (ap@nypop.com)
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  *
23  */
24
25 #include "config.h"
26 #include "core/html/HTMLTextFormControlElement.h"
27
28 #include "HTMLNames.h"
29 #include "bindings/v8/ExceptionState.h"
30 #include "bindings/v8/ExceptionStatePlaceholder.h"
31 #include "core/accessibility/AXObjectCache.h"
32 #include "core/dom/Document.h"
33 #include "core/dom/NodeTraversal.h"
34 #include "core/dom/Text.h"
35 #include "core/dom/shadow/ShadowRoot.h"
36 #include "core/editing/FrameSelection.h"
37 #include "core/editing/TextIterator.h"
38 #include "core/events/Event.h"
39 #include "core/events/ThreadLocalEventNames.h"
40 #include "core/html/HTMLBRElement.h"
41 #include "core/html/shadow/ShadowElementNames.h"
42 #include "core/frame/Frame.h"
43 #include "core/frame/UseCounter.h"
44 #include "core/rendering/RenderBlock.h"
45 #include "core/rendering/RenderTheme.h"
46 #include "wtf/text/StringBuilder.h"
47
48 namespace WebCore {
49
50 using namespace HTMLNames;
51 using namespace std;
52
53 HTMLTextFormControlElement::HTMLTextFormControlElement(const QualifiedName& tagName, Document& doc, HTMLFormElement* form)
54     : HTMLFormControlElementWithState(tagName, doc, form)
55     , m_lastChangeWasUserEdit(false)
56     , m_cachedSelectionStart(-1)
57     , m_cachedSelectionEnd(-1)
58     , m_cachedSelectionDirection(SelectionHasNoDirection)
59 {
60 }
61
62 HTMLTextFormControlElement::~HTMLTextFormControlElement()
63 {
64 }
65
66 Node::InsertionNotificationRequest HTMLTextFormControlElement::insertedInto(ContainerNode* insertionPoint)
67 {
68     HTMLFormControlElementWithState::insertedInto(insertionPoint);
69     if (!insertionPoint->inDocument())
70         return InsertionDone;
71     String initialValue = value();
72     setTextAsOfLastFormControlChangeEvent(initialValue.isNull() ? emptyString() : initialValue);
73     return InsertionDone;
74 }
75
76 void HTMLTextFormControlElement::dispatchFocusEvent(Element* oldFocusedElement, FocusType type)
77 {
78     if (supportsPlaceholder())
79         updatePlaceholderVisibility(false);
80     handleFocusEvent(oldFocusedElement, type);
81     HTMLFormControlElementWithState::dispatchFocusEvent(oldFocusedElement, type);
82 }
83
84 void HTMLTextFormControlElement::dispatchBlurEvent(Element* newFocusedElement)
85 {
86     if (supportsPlaceholder())
87         updatePlaceholderVisibility(false);
88     handleBlurEvent();
89     HTMLFormControlElementWithState::dispatchBlurEvent(newFocusedElement);
90 }
91
92 void HTMLTextFormControlElement::defaultEventHandler(Event* event)
93 {
94     if (event->type() == EventTypeNames::webkitEditableContentChanged && renderer() && renderer()->isTextControl()) {
95         m_lastChangeWasUserEdit = true;
96         subtreeHasChanged();
97         return;
98     }
99
100     HTMLFormControlElementWithState::defaultEventHandler(event);
101 }
102
103 void HTMLTextFormControlElement::forwardEvent(Event* event)
104 {
105     if (event->type() == EventTypeNames::blur || event->type() == EventTypeNames::focus)
106         return;
107     innerTextElement()->defaultEventHandler(event);
108 }
109
110 String HTMLTextFormControlElement::strippedPlaceholder() const
111 {
112     // According to the HTML5 specification, we need to remove CR and LF from
113     // the attribute value.
114     const AtomicString& attributeValue = fastGetAttribute(placeholderAttr);
115     if (!attributeValue.contains(newlineCharacter) && !attributeValue.contains(carriageReturn))
116         return attributeValue;
117
118     StringBuilder stripped;
119     unsigned length = attributeValue.length();
120     stripped.reserveCapacity(length);
121     for (unsigned i = 0; i < length; ++i) {
122         UChar character = attributeValue[i];
123         if (character == newlineCharacter || character == carriageReturn)
124             continue;
125         stripped.append(character);
126     }
127     return stripped.toString();
128 }
129
130 static bool isNotLineBreak(UChar ch) { return ch != newlineCharacter && ch != carriageReturn; }
131
132 bool HTMLTextFormControlElement::isPlaceholderEmpty() const
133 {
134     const AtomicString& attributeValue = fastGetAttribute(placeholderAttr);
135     return attributeValue.string().find(isNotLineBreak) == kNotFound;
136 }
137
138 bool HTMLTextFormControlElement::placeholderShouldBeVisible() const
139 {
140     return supportsPlaceholder()
141         && isEmptyValue()
142         && isEmptySuggestedValue()
143         && !isPlaceholderEmpty()
144         && (document().focusedElement() != this || (RenderTheme::theme().shouldShowPlaceholderWhenFocused()))
145         && (!renderer() || renderer()->style()->visibility() == VISIBLE);
146 }
147
148 HTMLElement* HTMLTextFormControlElement::placeholderElement() const
149 {
150     return toHTMLElement(userAgentShadowRoot()->getElementById(ShadowElementNames::placeholder()));
151 }
152
153 void HTMLTextFormControlElement::updatePlaceholderVisibility(bool placeholderValueChanged)
154 {
155     if (!supportsPlaceholder())
156         return;
157     if (!placeholderElement() || placeholderValueChanged)
158         updatePlaceholderText();
159     HTMLElement* placeholder = placeholderElement();
160     if (!placeholder)
161         return;
162     placeholder->setInlineStyleProperty(CSSPropertyVisibility, placeholderShouldBeVisible() ? CSSValueVisible : CSSValueHidden);
163 }
164
165 void HTMLTextFormControlElement::setSelectionStart(int start)
166 {
167     setSelectionRange(start, max(start, selectionEnd()), selectionDirection());
168 }
169
170 void HTMLTextFormControlElement::setSelectionEnd(int end)
171 {
172     setSelectionRange(min(end, selectionStart()), end, selectionDirection());
173 }
174
175 void HTMLTextFormControlElement::setSelectionDirection(const String& direction)
176 {
177     setSelectionRange(selectionStart(), selectionEnd(), direction);
178 }
179
180 void HTMLTextFormControlElement::select()
181 {
182     setSelectionRange(0, numeric_limits<int>::max(), SelectionHasNoDirection);
183 }
184
185 void HTMLTextFormControlElement::dispatchFormControlChangeEvent()
186 {
187     if (m_textAsOfLastFormControlChangeEvent != value()) {
188         dispatchChangeEvent();
189         setTextAsOfLastFormControlChangeEvent(value());
190     }
191     setChangedSinceLastFormControlChangeEvent(false);
192 }
193
194 static inline bool hasVisibleTextArea(RenderObject* renderer, HTMLElement* innerText)
195 {
196     ASSERT(renderer);
197     return renderer->style()->visibility() != HIDDEN && innerText && innerText->renderer() && innerText->renderBox()->height();
198 }
199
200
201 void HTMLTextFormControlElement::setRangeText(const String& replacement, ExceptionState& exceptionState)
202 {
203     setRangeText(replacement, selectionStart(), selectionEnd(), String(), exceptionState);
204 }
205
206 void HTMLTextFormControlElement::setRangeText(const String& replacement, unsigned start, unsigned end, const String& selectionMode, ExceptionState& exceptionState)
207 {
208     if (start > end) {
209         exceptionState.throwDOMException(IndexSizeError, "The provided start value (" + String::number(start) + ") is larger than the provided end value (" + String::number(end) + ").");
210         return;
211     }
212     if (hasAuthorShadowRoot())
213         return;
214
215     String text = innerTextValue();
216     unsigned textLength = text.length();
217     unsigned replacementLength = replacement.length();
218     unsigned newSelectionStart = selectionStart();
219     unsigned newSelectionEnd = selectionEnd();
220
221     start = std::min(start, textLength);
222     end = std::min(end, textLength);
223
224     if (start < end)
225         text.replace(start, end - start, replacement);
226     else
227         text.insert(replacement, start);
228
229     setInnerTextValue(text);
230
231     // FIXME: What should happen to the value (as in value()) if there's no renderer?
232     if (!renderer())
233         return;
234
235     subtreeHasChanged();
236
237     if (equalIgnoringCase(selectionMode, "select")) {
238         newSelectionStart = start;
239         newSelectionEnd = start + replacementLength;
240     } else if (equalIgnoringCase(selectionMode, "start"))
241         newSelectionStart = newSelectionEnd = start;
242     else if (equalIgnoringCase(selectionMode, "end"))
243         newSelectionStart = newSelectionEnd = start + replacementLength;
244     else {
245         // Default is "preserve".
246         long delta = replacementLength - (end - start);
247
248         if (newSelectionStart > end)
249             newSelectionStart += delta;
250         else if (newSelectionStart > start)
251             newSelectionStart = start;
252
253         if (newSelectionEnd > end)
254             newSelectionEnd += delta;
255         else if (newSelectionEnd > start)
256             newSelectionEnd = start + replacementLength;
257     }
258
259     setSelectionRange(newSelectionStart, newSelectionEnd, SelectionHasNoDirection);
260 }
261
262 void HTMLTextFormControlElement::setSelectionRange(int start, int end, const String& directionString)
263 {
264     TextFieldSelectionDirection direction = SelectionHasNoDirection;
265     if (directionString == "forward")
266         direction = SelectionHasForwardDirection;
267     else if (directionString == "backward")
268         direction = SelectionHasBackwardDirection;
269
270     return setSelectionRange(start, end, direction);
271 }
272
273 void HTMLTextFormControlElement::setSelectionRange(int start, int end, TextFieldSelectionDirection direction)
274 {
275     document().updateLayoutIgnorePendingStylesheets();
276
277     if (!renderer() || !renderer()->isTextControl())
278         return;
279
280     end = max(end, 0);
281     start = min(max(start, 0), end);
282
283     if (!hasVisibleTextArea(renderer(), innerTextElement())) {
284         cacheSelection(start, end, direction);
285         return;
286     }
287     VisiblePosition startPosition = visiblePositionForIndex(start);
288     VisiblePosition endPosition;
289     if (start == end)
290         endPosition = startPosition;
291     else
292         endPosition = visiblePositionForIndex(end);
293
294     // startPosition and endPosition can be null position for example when
295     // "-webkit-user-select: none" style attribute is specified.
296     if (startPosition.isNotNull() && endPosition.isNotNull()) {
297         ASSERT(startPosition.deepEquivalent().deprecatedNode()->shadowHost() == this
298             && endPosition.deepEquivalent().deprecatedNode()->shadowHost() == this);
299     }
300     VisibleSelection newSelection;
301     if (direction == SelectionHasBackwardDirection)
302         newSelection = VisibleSelection(endPosition, startPosition);
303     else
304         newSelection = VisibleSelection(startPosition, endPosition);
305     newSelection.setIsDirectional(direction != SelectionHasNoDirection);
306
307     if (Frame* frame = document().frame())
308         frame->selection().setSelection(newSelection);
309 }
310
311 VisiblePosition HTMLTextFormControlElement::visiblePositionForIndex(int index) const
312 {
313     if (index <= 0)
314         return VisiblePosition(firstPositionInNode(innerTextElement()), DOWNSTREAM);
315     RefPtr<Range> range = Range::create(document());
316     range->selectNodeContents(innerTextElement(), ASSERT_NO_EXCEPTION);
317     CharacterIterator it(range.get());
318     it.advance(index - 1);
319     return VisiblePosition(it.range()->endPosition(), UPSTREAM);
320 }
321
322 int HTMLTextFormControlElement::indexForVisiblePosition(const VisiblePosition& pos) const
323 {
324     Position indexPosition = pos.deepEquivalent().parentAnchoredEquivalent();
325     if (enclosingTextFormControl(indexPosition) != this)
326         return 0;
327     ASSERT(indexPosition.document());
328     RefPtr<Range> range = Range::create(*indexPosition.document());
329     range->setStart(innerTextElement(), 0, ASSERT_NO_EXCEPTION);
330     range->setEnd(indexPosition.containerNode(), indexPosition.offsetInContainerNode(), ASSERT_NO_EXCEPTION);
331     return TextIterator::rangeLength(range.get());
332 }
333
334 int HTMLTextFormControlElement::selectionStart() const
335 {
336     if (!isTextFormControl())
337         return 0;
338     if (document().focusedElement() != this && hasCachedSelection())
339         return m_cachedSelectionStart;
340
341     return computeSelectionStart();
342 }
343
344 int HTMLTextFormControlElement::computeSelectionStart() const
345 {
346     ASSERT(isTextFormControl());
347     Frame* frame = document().frame();
348     if (!frame)
349         return 0;
350
351     return indexForVisiblePosition(frame->selection().start());
352 }
353
354 int HTMLTextFormControlElement::selectionEnd() const
355 {
356     if (!isTextFormControl())
357         return 0;
358     if (document().focusedElement() != this && hasCachedSelection())
359         return m_cachedSelectionEnd;
360     return computeSelectionEnd();
361 }
362
363 int HTMLTextFormControlElement::computeSelectionEnd() const
364 {
365     ASSERT(isTextFormControl());
366     Frame* frame = document().frame();
367     if (!frame)
368         return 0;
369
370     return indexForVisiblePosition(frame->selection().end());
371 }
372
373 static const AtomicString& directionString(TextFieldSelectionDirection direction)
374 {
375     DEFINE_STATIC_LOCAL(const AtomicString, none, ("none", AtomicString::ConstructFromLiteral));
376     DEFINE_STATIC_LOCAL(const AtomicString, forward, ("forward", AtomicString::ConstructFromLiteral));
377     DEFINE_STATIC_LOCAL(const AtomicString, backward, ("backward", AtomicString::ConstructFromLiteral));
378
379     switch (direction) {
380     case SelectionHasNoDirection:
381         return none;
382     case SelectionHasForwardDirection:
383         return forward;
384     case SelectionHasBackwardDirection:
385         return backward;
386     }
387
388     ASSERT_NOT_REACHED();
389     return none;
390 }
391
392 const AtomicString& HTMLTextFormControlElement::selectionDirection() const
393 {
394     if (!isTextFormControl())
395         return directionString(SelectionHasNoDirection);
396     if (document().focusedElement() != this && hasCachedSelection())
397         return directionString(m_cachedSelectionDirection);
398
399     return directionString(computeSelectionDirection());
400 }
401
402 TextFieldSelectionDirection HTMLTextFormControlElement::computeSelectionDirection() const
403 {
404     ASSERT(isTextFormControl());
405     Frame* frame = document().frame();
406     if (!frame)
407         return SelectionHasNoDirection;
408
409     const VisibleSelection& selection = frame->selection().selection();
410     return selection.isDirectional() ? (selection.isBaseFirst() ? SelectionHasForwardDirection : SelectionHasBackwardDirection) : SelectionHasNoDirection;
411 }
412
413 static inline void setContainerAndOffsetForRange(Node* node, int offset, Node*& containerNode, int& offsetInContainer)
414 {
415     if (node->isTextNode()) {
416         containerNode = node;
417         offsetInContainer = offset;
418     } else {
419         containerNode = node->parentNode();
420         offsetInContainer = node->nodeIndex() + offset;
421     }
422 }
423
424 PassRefPtr<Range> HTMLTextFormControlElement::selection() const
425 {
426     if (!renderer() || !isTextFormControl() || !hasCachedSelection())
427         return 0;
428
429     int start = m_cachedSelectionStart;
430     int end = m_cachedSelectionEnd;
431
432     ASSERT(start <= end);
433     HTMLElement* innerText = innerTextElement();
434     if (!innerText)
435         return 0;
436
437     if (!innerText->firstChild())
438         return Range::create(document(), innerText, 0, innerText, 0);
439
440     int offset = 0;
441     Node* startNode = 0;
442     Node* endNode = 0;
443     for (Node* node = innerText->firstChild(); node; node = NodeTraversal::next(*node, innerText)) {
444         ASSERT(!node->firstChild());
445         ASSERT(node->isTextNode() || node->hasTagName(brTag));
446         int length = node->isTextNode() ? lastOffsetInNode(node) : 1;
447
448         if (offset <= start && start <= offset + length)
449             setContainerAndOffsetForRange(node, start - offset, startNode, start);
450
451         if (offset <= end && end <= offset + length) {
452             setContainerAndOffsetForRange(node, end - offset, endNode, end);
453             break;
454         }
455
456         offset += length;
457     }
458
459     if (!startNode || !endNode)
460         return 0;
461
462     return Range::create(document(), startNode, start, endNode, end);
463 }
464
465 void HTMLTextFormControlElement::restoreCachedSelection()
466 {
467     setSelectionRange(m_cachedSelectionStart, m_cachedSelectionEnd, m_cachedSelectionDirection);
468 }
469
470 void HTMLTextFormControlElement::selectionChanged(bool userTriggered)
471 {
472     if (!renderer() || !isTextFormControl())
473         return;
474
475     // selectionStart() or selectionEnd() will return cached selection when this node doesn't have focus
476     cacheSelection(computeSelectionStart(), computeSelectionEnd(), computeSelectionDirection());
477
478     if (Frame* frame = document().frame()) {
479         if (frame->selection().isRange() && userTriggered)
480             dispatchEvent(Event::createBubble(EventTypeNames::select));
481     }
482 }
483
484 void HTMLTextFormControlElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
485 {
486     if (name == placeholderAttr) {
487         updatePlaceholderVisibility(true);
488         UseCounter::count(document(), UseCounter::PlaceholderAttribute);
489     } else
490         HTMLFormControlElementWithState::parseAttribute(name, value);
491 }
492
493 bool HTMLTextFormControlElement::lastChangeWasUserEdit() const
494 {
495     if (!isTextFormControl())
496         return false;
497     return m_lastChangeWasUserEdit;
498 }
499
500 void HTMLTextFormControlElement::setInnerTextValue(const String& value)
501 {
502     ASSERT(!hasAuthorShadowRoot());
503     if (!isTextFormControl() || hasAuthorShadowRoot())
504         return;
505
506     bool textIsChanged = value != innerTextValue();
507     if (textIsChanged || !innerTextElement()->hasChildNodes()) {
508         if (textIsChanged && renderer()) {
509             if (AXObjectCache* cache = document().existingAXObjectCache())
510                 cache->postNotification(this, AXObjectCache::AXValueChanged, false);
511         }
512         innerTextElement()->setInnerText(value, ASSERT_NO_EXCEPTION);
513
514         if (value.endsWith('\n') || value.endsWith('\r'))
515             innerTextElement()->appendChild(HTMLBRElement::create(document()));
516     }
517
518     setFormControlValueMatchesRenderer(true);
519 }
520
521 static String finishText(StringBuilder& result)
522 {
523     // Remove one trailing newline; there's always one that's collapsed out by rendering.
524     size_t size = result.length();
525     if (size && result[size - 1] == '\n')
526         result.resize(--size);
527     return result.toString();
528 }
529
530 String HTMLTextFormControlElement::innerTextValue() const
531 {
532     ASSERT(!hasAuthorShadowRoot());
533     HTMLElement* innerText = innerTextElement();
534     if (!innerText || !isTextFormControl())
535         return emptyString();
536
537     StringBuilder result;
538     for (Node* node = innerText; node; node = NodeTraversal::next(*node, innerText)) {
539         if (node->hasTagName(brTag))
540             result.append(newlineCharacter);
541         else if (node->isTextNode())
542             result.append(toText(node)->data());
543     }
544     return finishText(result);
545 }
546
547 static void getNextSoftBreak(RootInlineBox*& line, Node*& breakNode, unsigned& breakOffset)
548 {
549     RootInlineBox* next;
550     for (; line; line = next) {
551         next = line->nextRootBox();
552         if (next && !line->endsWithBreak()) {
553             ASSERT(line->lineBreakObj());
554             breakNode = line->lineBreakObj()->node();
555             breakOffset = line->lineBreakPos();
556             line = next;
557             return;
558         }
559     }
560     breakNode = 0;
561     breakOffset = 0;
562 }
563
564 String HTMLTextFormControlElement::valueWithHardLineBreaks() const
565 {
566     // FIXME: It's not acceptable to ignore the HardWrap setting when there is no renderer.
567     // While we have no evidence this has ever been a practical problem, it would be best to fix it some day.
568     HTMLElement* innerText = innerTextElement();
569     if (!innerText || !isTextFormControl())
570         return value();
571
572     RenderBlock* renderer = toRenderBlock(innerText->renderer());
573     if (!renderer)
574         return value();
575
576     Node* breakNode;
577     unsigned breakOffset;
578     RootInlineBox* line = renderer->firstRootBox();
579     if (!line)
580         return value();
581
582     getNextSoftBreak(line, breakNode, breakOffset);
583
584     StringBuilder result;
585     for (Node* node = innerText->firstChild(); node; node = NodeTraversal::next(*node, innerText)) {
586         if (node->hasTagName(brTag))
587             result.append(newlineCharacter);
588         else if (node->isTextNode()) {
589             String data = toText(node)->data();
590             unsigned length = data.length();
591             unsigned position = 0;
592             while (breakNode == node && breakOffset <= length) {
593                 if (breakOffset > position) {
594                     result.append(data, position, breakOffset - position);
595                     position = breakOffset;
596                     result.append(newlineCharacter);
597                 }
598                 getNextSoftBreak(line, breakNode, breakOffset);
599             }
600             result.append(data, position, length - position);
601         }
602         while (breakNode == node)
603             getNextSoftBreak(line, breakNode, breakOffset);
604     }
605     return finishText(result);
606 }
607
608 HTMLTextFormControlElement* enclosingTextFormControl(const Position& position)
609 {
610     ASSERT(position.isNull() || position.anchorType() == Position::PositionIsOffsetInAnchor
611         || position.containerNode() || !position.anchorNode()->shadowHost()
612         || (position.anchorNode()->parentNode() && position.anchorNode()->parentNode()->isShadowRoot()));
613
614     Node* container = position.containerNode();
615     if (!container)
616         return 0;
617     Element* ancestor = container->shadowHost();
618     return ancestor && isHTMLTextFormControlElement(*ancestor) ? toHTMLTextFormControlElement(ancestor) : 0;
619 }
620
621 static const HTMLElement* parentHTMLElement(const Element* element)
622 {
623     while (element) {
624         element = element->parentElement();
625         if (element && element->isHTMLElement())
626             return toHTMLElement(element);
627     }
628     return 0;
629 }
630
631 String HTMLTextFormControlElement::directionForFormData() const
632 {
633     for (const HTMLElement* element = this; element; element = parentHTMLElement(element)) {
634         const AtomicString& dirAttributeValue = element->fastGetAttribute(dirAttr);
635         if (dirAttributeValue.isNull())
636             continue;
637
638         if (equalIgnoringCase(dirAttributeValue, "rtl") || equalIgnoringCase(dirAttributeValue, "ltr"))
639             return dirAttributeValue;
640
641         if (equalIgnoringCase(dirAttributeValue, "auto")) {
642             bool isAuto;
643             TextDirection textDirection = element->directionalityIfhasDirAutoAttribute(isAuto);
644             return textDirection == RTL ? "rtl" : "ltr";
645         }
646     }
647
648     return "ltr";
649 }
650
651 HTMLElement* HTMLTextFormControlElement::innerTextElement() const
652 {
653     return toHTMLElement(userAgentShadowRoot()->getElementById(ShadowElementNames::innerEditor()));
654 }
655
656 } // namespace Webcore