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