summaryrefslogtreecommitdiffstats
path: root/dom/xslt/xpath/txMozillaXPathTreeWalker.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/xslt/xpath/txMozillaXPathTreeWalker.cpp')
-rw-r--r--dom/xslt/xpath/txMozillaXPathTreeWalker.cpp776
1 files changed, 776 insertions, 0 deletions
diff --git a/dom/xslt/xpath/txMozillaXPathTreeWalker.cpp b/dom/xslt/xpath/txMozillaXPathTreeWalker.cpp
new file mode 100644
index 000000000..1387097e1
--- /dev/null
+++ b/dom/xslt/xpath/txMozillaXPathTreeWalker.cpp
@@ -0,0 +1,776 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "txXPathTreeWalker.h"
+#include "nsIAtom.h"
+#include "nsIAttribute.h"
+#include "nsIDOMAttr.h"
+#include "nsIDOMDocument.h"
+#include "nsIDOMNode.h"
+#include "nsIDOMElement.h"
+#include "nsIDOMProcessingInstruction.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTextFragment.h"
+#include "txXMLUtils.h"
+#include "txLog.h"
+#include "nsUnicharUtils.h"
+#include "nsAttrName.h"
+#include "nsTArray.h"
+#include "mozilla/dom/Attr.h"
+#include "mozilla/dom/Element.h"
+#include <stdint.h>
+#include <algorithm>
+
+using namespace mozilla::dom;
+
+const uint32_t kUnknownIndex = uint32_t(-1);
+
+txXPathTreeWalker::txXPathTreeWalker(const txXPathTreeWalker& aOther)
+ : mPosition(aOther.mPosition),
+ mCurrentIndex(aOther.mCurrentIndex)
+{
+}
+
+txXPathTreeWalker::txXPathTreeWalker(const txXPathNode& aNode)
+ : mPosition(aNode),
+ mCurrentIndex(kUnknownIndex)
+{
+}
+
+void
+txXPathTreeWalker::moveToRoot()
+{
+ if (mPosition.isDocument()) {
+ return;
+ }
+
+ nsIDocument* root = mPosition.mNode->GetUncomposedDoc();
+ if (root) {
+ mPosition.mIndex = txXPathNode::eDocument;
+ mPosition.mNode = root;
+ }
+ else {
+ nsINode *rootNode = mPosition.Root();
+
+ NS_ASSERTION(rootNode->IsNodeOfType(nsINode::eCONTENT),
+ "root of subtree wasn't an nsIContent");
+
+ mPosition.mIndex = txXPathNode::eContent;
+ mPosition.mNode = rootNode;
+ }
+
+ mCurrentIndex = kUnknownIndex;
+ mDescendants.Clear();
+}
+
+bool
+txXPathTreeWalker::moveToElementById(const nsAString& aID)
+{
+ if (aID.IsEmpty()) {
+ return false;
+ }
+
+ nsIDocument* doc = mPosition.mNode->GetUncomposedDoc();
+
+ nsCOMPtr<nsIContent> content;
+ if (doc) {
+ content = doc->GetElementById(aID);
+ }
+ else {
+ // We're in a disconnected subtree, search only that subtree.
+ nsINode *rootNode = mPosition.Root();
+
+ NS_ASSERTION(rootNode->IsNodeOfType(nsINode::eCONTENT),
+ "root of subtree wasn't an nsIContent");
+
+ content = nsContentUtils::MatchElementId(
+ static_cast<nsIContent*>(rootNode), aID);
+ }
+
+ if (!content) {
+ return false;
+ }
+
+ mPosition.mIndex = txXPathNode::eContent;
+ mPosition.mNode = content;
+ mCurrentIndex = kUnknownIndex;
+ mDescendants.Clear();
+
+ return true;
+}
+
+bool
+txXPathTreeWalker::moveToFirstAttribute()
+{
+ if (!mPosition.isContent()) {
+ return false;
+ }
+
+ return moveToValidAttribute(0);
+}
+
+bool
+txXPathTreeWalker::moveToNextAttribute()
+{
+ // XXX an assertion should be enough here with the current code
+ if (!mPosition.isAttribute()) {
+ return false;
+ }
+
+ return moveToValidAttribute(mPosition.mIndex + 1);
+}
+
+bool
+txXPathTreeWalker::moveToValidAttribute(uint32_t aStartIndex)
+{
+ NS_ASSERTION(!mPosition.isDocument(), "documents doesn't have attrs");
+
+ uint32_t total = mPosition.Content()->GetAttrCount();
+ if (aStartIndex >= total) {
+ return false;
+ }
+
+ uint32_t index;
+ for (index = aStartIndex; index < total; ++index) {
+ const nsAttrName* name = mPosition.Content()->GetAttrNameAt(index);
+
+ // We need to ignore XMLNS attributes.
+ if (name->NamespaceID() != kNameSpaceID_XMLNS) {
+ mPosition.mIndex = index;
+
+ return true;
+ }
+ }
+ return false;
+}
+
+bool
+txXPathTreeWalker::moveToNamedAttribute(nsIAtom* aLocalName, int32_t aNSID)
+{
+ if (!mPosition.isContent()) {
+ return false;
+ }
+
+ const nsAttrName* name;
+ uint32_t i;
+ for (i = 0; (name = mPosition.Content()->GetAttrNameAt(i)); ++i) {
+ if (name->Equals(aLocalName, aNSID)) {
+ mPosition.mIndex = i;
+
+ return true;
+ }
+ }
+ return false;
+}
+
+bool
+txXPathTreeWalker::moveToFirstChild()
+{
+ if (mPosition.isAttribute()) {
+ return false;
+ }
+
+ NS_ASSERTION(!mPosition.isDocument() ||
+ (mCurrentIndex == kUnknownIndex && mDescendants.IsEmpty()),
+ "we shouldn't have any position info at the document");
+ NS_ASSERTION(mCurrentIndex != kUnknownIndex || mDescendants.IsEmpty(),
+ "Index should be known if parents index are");
+
+ nsIContent* child = mPosition.mNode->GetFirstChild();
+ if (!child) {
+ return false;
+ }
+ mPosition.mIndex = txXPathNode::eContent;
+ mPosition.mNode = child;
+
+ if (mCurrentIndex != kUnknownIndex &&
+ !mDescendants.AppendValue(mCurrentIndex)) {
+ mDescendants.Clear();
+ }
+ mCurrentIndex = 0;
+
+ return true;
+}
+
+bool
+txXPathTreeWalker::moveToLastChild()
+{
+ if (mPosition.isAttribute()) {
+ return false;
+ }
+
+ NS_ASSERTION(!mPosition.isDocument() ||
+ (mCurrentIndex == kUnknownIndex && mDescendants.IsEmpty()),
+ "we shouldn't have any position info at the document");
+ NS_ASSERTION(mCurrentIndex != kUnknownIndex || mDescendants.IsEmpty(),
+ "Index should be known if parents index are");
+
+ uint32_t total = mPosition.mNode->GetChildCount();
+ if (!total) {
+ return false;
+ }
+ mPosition.mNode = mPosition.mNode->GetLastChild();
+
+ if (mCurrentIndex != kUnknownIndex &&
+ !mDescendants.AppendValue(mCurrentIndex)) {
+ mDescendants.Clear();
+ }
+ mCurrentIndex = total - 1;
+
+ return true;
+}
+
+bool
+txXPathTreeWalker::moveToNextSibling()
+{
+ if (!mPosition.isContent()) {
+ return false;
+ }
+
+ return moveToSibling(1);
+}
+
+bool
+txXPathTreeWalker::moveToPreviousSibling()
+{
+ if (!mPosition.isContent()) {
+ return false;
+ }
+
+ return moveToSibling(-1);
+}
+
+bool
+txXPathTreeWalker::moveToParent()
+{
+ if (mPosition.isDocument()) {
+ return false;
+ }
+
+ if (mPosition.isAttribute()) {
+ mPosition.mIndex = txXPathNode::eContent;
+
+ return true;
+ }
+
+ nsINode* parent = mPosition.mNode->GetParentNode();
+ if (!parent) {
+ return false;
+ }
+
+ uint32_t count = mDescendants.Length();
+ if (count) {
+ mCurrentIndex = mDescendants.ValueAt(--count);
+ mDescendants.RemoveValueAt(count);
+ }
+ else {
+ mCurrentIndex = kUnknownIndex;
+ }
+
+ mPosition.mIndex = mPosition.mNode->GetParent() ?
+ txXPathNode::eContent : txXPathNode::eDocument;
+ mPosition.mNode = parent;
+
+ return true;
+}
+
+bool
+txXPathTreeWalker::moveToSibling(int32_t aDir)
+{
+ NS_ASSERTION(mPosition.isContent(),
+ "moveToSibling should only be called for content");
+
+ nsINode* parent = mPosition.mNode->GetParentNode();
+ if (!parent) {
+ return false;
+ }
+ if (mCurrentIndex == kUnknownIndex) {
+ mCurrentIndex = parent->IndexOf(mPosition.mNode);
+ }
+
+ // if mCurrentIndex is 0 we rely on GetChildAt returning null for an
+ // index of uint32_t(-1).
+ uint32_t newIndex = mCurrentIndex + aDir;
+ nsIContent* newChild = parent->GetChildAt(newIndex);
+ if (!newChild) {
+ return false;
+ }
+
+ mPosition.mNode = newChild;
+ mCurrentIndex = newIndex;
+
+ return true;
+}
+
+txXPathNode::txXPathNode(const txXPathNode& aNode)
+ : mNode(aNode.mNode),
+ mRefCountRoot(aNode.mRefCountRoot),
+ mIndex(aNode.mIndex)
+{
+ MOZ_COUNT_CTOR(txXPathNode);
+ if (mRefCountRoot) {
+ NS_ADDREF(Root());
+ }
+}
+
+txXPathNode::~txXPathNode()
+{
+ MOZ_COUNT_DTOR(txXPathNode);
+ if (mRefCountRoot) {
+ nsINode *root = Root();
+ NS_RELEASE(root);
+ }
+}
+
+/* static */
+bool
+txXPathNodeUtils::getAttr(const txXPathNode& aNode, nsIAtom* aLocalName,
+ int32_t aNSID, nsAString& aValue)
+{
+ if (aNode.isDocument() || aNode.isAttribute()) {
+ return false;
+ }
+
+ return aNode.Content()->GetAttr(aNSID, aLocalName, aValue);
+}
+
+/* static */
+already_AddRefed<nsIAtom>
+txXPathNodeUtils::getLocalName(const txXPathNode& aNode)
+{
+ if (aNode.isDocument()) {
+ return nullptr;
+ }
+
+ if (aNode.isContent()) {
+ if (aNode.mNode->IsElement()) {
+ nsCOMPtr<nsIAtom> localName =
+ aNode.Content()->NodeInfo()->NameAtom();
+ return localName.forget();
+ }
+
+ if (aNode.mNode->IsNodeOfType(nsINode::ePROCESSING_INSTRUCTION)) {
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(aNode.mNode);
+ nsAutoString target;
+ node->GetNodeName(target);
+
+ return NS_Atomize(target);
+ }
+
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAtom> localName = aNode.Content()->
+ GetAttrNameAt(aNode.mIndex)->LocalName();
+
+ return localName.forget();
+}
+
+nsIAtom*
+txXPathNodeUtils::getPrefix(const txXPathNode& aNode)
+{
+ if (aNode.isDocument()) {
+ return nullptr;
+ }
+
+ if (aNode.isContent()) {
+ // All other nsIContent node types but elements have a null prefix
+ // which is what we want here.
+ return aNode.Content()->NodeInfo()->GetPrefixAtom();
+ }
+
+ return aNode.Content()->GetAttrNameAt(aNode.mIndex)->GetPrefix();
+}
+
+/* static */
+void
+txXPathNodeUtils::getLocalName(const txXPathNode& aNode, nsAString& aLocalName)
+{
+ if (aNode.isDocument()) {
+ aLocalName.Truncate();
+
+ return;
+ }
+
+ if (aNode.isContent()) {
+ if (aNode.mNode->IsElement()) {
+ mozilla::dom::NodeInfo* nodeInfo = aNode.Content()->NodeInfo();
+ nodeInfo->GetName(aLocalName);
+ return;
+ }
+
+ if (aNode.mNode->IsNodeOfType(nsINode::ePROCESSING_INSTRUCTION)) {
+ // PIs don't have a nodeinfo but do have a name
+ nsCOMPtr<nsIDOMNode> node = do_QueryInterface(aNode.mNode);
+ node->GetNodeName(aLocalName);
+
+ return;
+ }
+
+ aLocalName.Truncate();
+
+ return;
+ }
+
+ aNode.Content()->GetAttrNameAt(aNode.mIndex)->LocalName()->
+ ToString(aLocalName);
+
+ // Check for html
+ if (aNode.Content()->NodeInfo()->NamespaceEquals(kNameSpaceID_None) &&
+ aNode.Content()->IsHTMLElement()) {
+ nsContentUtils::ASCIIToUpper(aLocalName);
+ }
+}
+
+/* static */
+void
+txXPathNodeUtils::getNodeName(const txXPathNode& aNode, nsAString& aName)
+{
+ if (aNode.isDocument()) {
+ aName.Truncate();
+
+ return;
+ }
+
+ if (aNode.isContent()) {
+ // Elements and PIs have a name
+ if (aNode.mNode->IsElement() ||
+ aNode.mNode->NodeType() ==
+ nsIDOMNode::PROCESSING_INSTRUCTION_NODE) {
+ aName = aNode.Content()->NodeName();
+ return;
+ }
+
+ aName.Truncate();
+
+ return;
+ }
+
+ aNode.Content()->GetAttrNameAt(aNode.mIndex)->GetQualifiedName(aName);
+}
+
+/* static */
+int32_t
+txXPathNodeUtils::getNamespaceID(const txXPathNode& aNode)
+{
+ if (aNode.isDocument()) {
+ return kNameSpaceID_None;
+ }
+
+ if (aNode.isContent()) {
+ return aNode.Content()->GetNameSpaceID();
+ }
+
+ return aNode.Content()->GetAttrNameAt(aNode.mIndex)->NamespaceID();
+}
+
+/* static */
+void
+txXPathNodeUtils::getNamespaceURI(const txXPathNode& aNode, nsAString& aURI)
+{
+ nsContentUtils::NameSpaceManager()->GetNameSpaceURI(getNamespaceID(aNode), aURI);
+}
+
+/* static */
+uint16_t
+txXPathNodeUtils::getNodeType(const txXPathNode& aNode)
+{
+ if (aNode.isDocument()) {
+ return txXPathNodeType::DOCUMENT_NODE;
+ }
+
+ if (aNode.isContent()) {
+ return aNode.mNode->NodeType();
+ }
+
+ return txXPathNodeType::ATTRIBUTE_NODE;
+}
+
+/* static */
+void
+txXPathNodeUtils::appendNodeValue(const txXPathNode& aNode, nsAString& aResult)
+{
+ if (aNode.isAttribute()) {
+ const nsAttrName* name = aNode.Content()->GetAttrNameAt(aNode.mIndex);
+
+ if (aResult.IsEmpty()) {
+ aNode.Content()->GetAttr(name->NamespaceID(), name->LocalName(),
+ aResult);
+ }
+ else {
+ nsAutoString result;
+ aNode.Content()->GetAttr(name->NamespaceID(), name->LocalName(),
+ result);
+ aResult.Append(result);
+ }
+
+ return;
+ }
+
+ if (aNode.isDocument() ||
+ aNode.mNode->IsElement() ||
+ aNode.mNode->IsNodeOfType(nsINode::eDOCUMENT_FRAGMENT)) {
+ nsContentUtils::AppendNodeTextContent(aNode.mNode, true, aResult,
+ mozilla::fallible);
+
+ return;
+ }
+
+ aNode.Content()->AppendTextTo(aResult);
+}
+
+/* static */
+bool
+txXPathNodeUtils::isWhitespace(const txXPathNode& aNode)
+{
+ NS_ASSERTION(aNode.isContent() && isText(aNode), "Wrong type!");
+
+ return aNode.Content()->TextIsOnlyWhitespace();
+}
+
+/* static */
+txXPathNode*
+txXPathNodeUtils::getOwnerDocument(const txXPathNode& aNode)
+{
+ return new txXPathNode(aNode.mNode->OwnerDoc());
+}
+
+const char gPrintfFmt[] = "id0x%p";
+const char gPrintfFmtAttr[] = "id0x%p-%010i";
+
+/* static */
+nsresult
+txXPathNodeUtils::getXSLTId(const txXPathNode& aNode,
+ const txXPathNode& aBase,
+ nsAString& aResult)
+{
+ uintptr_t nodeid = ((uintptr_t)aNode.mNode) - ((uintptr_t)aBase.mNode);
+ if (!aNode.isAttribute()) {
+ CopyASCIItoUTF16(nsPrintfCString(gPrintfFmt, nodeid),
+ aResult);
+ }
+ else {
+ CopyASCIItoUTF16(nsPrintfCString(gPrintfFmtAttr,
+ nodeid, aNode.mIndex), aResult);
+ }
+
+ return NS_OK;
+}
+
+/* static */
+nsresult
+txXPathNodeUtils::getBaseURI(const txXPathNode& aNode, nsAString& aURI)
+{
+ return aNode.mNode->GetBaseURI(aURI);
+}
+
+/* static */
+int
+txXPathNodeUtils::comparePosition(const txXPathNode& aNode,
+ const txXPathNode& aOtherNode)
+{
+ // First check for equal nodes or attribute-nodes on the same element.
+ if (aNode.mNode == aOtherNode.mNode) {
+ if (aNode.mIndex == aOtherNode.mIndex) {
+ return 0;
+ }
+
+ NS_ASSERTION(!aNode.isDocument() && !aOtherNode.isDocument(),
+ "documents should always have a set index");
+
+ if (aNode.isContent() || (!aOtherNode.isContent() &&
+ aNode.mIndex < aOtherNode.mIndex)) {
+ return -1;
+ }
+
+ return 1;
+ }
+
+ // Get document for both nodes.
+ nsIDocument* document = aNode.mNode->GetUncomposedDoc();
+ nsIDocument* otherDocument = aOtherNode.mNode->GetUncomposedDoc();
+
+ // If the nodes have different current documents, compare the document
+ // pointers.
+ if (document != otherDocument) {
+ return document < otherDocument ? -1 : 1;
+ }
+
+ // Now either both nodes are in orphan trees, or they are both in the
+ // same tree.
+
+ // Get parents up the tree.
+ AutoTArray<nsINode*, 8> parents, otherParents;
+ nsINode* node = aNode.mNode;
+ nsINode* otherNode = aOtherNode.mNode;
+ nsINode* parent;
+ nsINode* otherParent;
+ while (node && otherNode) {
+ parent = node->GetParentNode();
+ otherParent = otherNode->GetParentNode();
+
+ // Hopefully this is a common case.
+ if (parent == otherParent) {
+ if (!parent) {
+ // Both node and otherNode are root nodes in respective orphan
+ // tree.
+ return node < otherNode ? -1 : 1;
+ }
+
+ return parent->IndexOf(node) < parent->IndexOf(otherNode) ?
+ -1 : 1;
+ }
+
+ parents.AppendElement(node);
+ otherParents.AppendElement(otherNode);
+ node = parent;
+ otherNode = otherParent;
+ }
+
+ while (node) {
+ parents.AppendElement(node);
+ node = node->GetParentNode();
+ }
+ while (otherNode) {
+ otherParents.AppendElement(otherNode);
+ otherNode = otherNode->GetParentNode();
+ }
+
+ // Walk back down along the parent-chains until we find where they split.
+ int32_t total = parents.Length() - 1;
+ int32_t otherTotal = otherParents.Length() - 1;
+ NS_ASSERTION(total != otherTotal, "Can't have same number of parents");
+
+ int32_t lastIndex = std::min(total, otherTotal);
+ int32_t i;
+ parent = nullptr;
+ for (i = 0; i <= lastIndex; ++i) {
+ node = parents.ElementAt(total - i);
+ otherNode = otherParents.ElementAt(otherTotal - i);
+ if (node != otherNode) {
+ if (!parent) {
+ // The two nodes are in different orphan subtrees.
+ NS_ASSERTION(i == 0, "this shouldn't happen");
+ return node < otherNode ? -1 : 1;
+ }
+
+ int32_t index = parent->IndexOf(node);
+ int32_t otherIndex = parent->IndexOf(otherNode);
+ NS_ASSERTION(index != otherIndex && index >= 0 && otherIndex >= 0,
+ "invalid index in compareTreePosition");
+
+ return index < otherIndex ? -1 : 1;
+ }
+
+ parent = node;
+ }
+
+ // One node is a descendant of the other. The one with the shortest
+ // parent-chain is first in the document.
+ return total < otherTotal ? -1 : 1;
+}
+
+/* static */
+txXPathNode*
+txXPathNativeNode::createXPathNode(nsIContent* aContent, bool aKeepRootAlive)
+{
+ nsINode* root = aKeepRootAlive ? txXPathNode::RootOf(aContent) : nullptr;
+
+ return new txXPathNode(aContent, txXPathNode::eContent, root);
+}
+
+/* static */
+txXPathNode*
+txXPathNativeNode::createXPathNode(nsINode* aNode, bool aKeepRootAlive)
+{
+ uint16_t nodeType = aNode->NodeType();
+ if (nodeType == nsIDOMNode::ATTRIBUTE_NODE) {
+ nsCOMPtr<nsIAttribute> attr = do_QueryInterface(aNode);
+ NS_ASSERTION(attr, "doesn't implement nsIAttribute");
+
+ mozilla::dom::NodeInfo *nodeInfo = attr->NodeInfo();
+ mozilla::dom::Element* parent =
+ static_cast<Attr*>(attr.get())->GetElement();
+ if (!parent) {
+ return nullptr;
+ }
+
+ nsINode* root = aKeepRootAlive ? txXPathNode::RootOf(parent) : nullptr;
+
+ uint32_t i, total = parent->GetAttrCount();
+ for (i = 0; i < total; ++i) {
+ const nsAttrName* name = parent->GetAttrNameAt(i);
+ if (nodeInfo->Equals(name->LocalName(), name->NamespaceID())) {
+ return new txXPathNode(parent, i, root);
+ }
+ }
+
+ NS_ERROR("Couldn't find the attribute in its parent!");
+
+ return nullptr;
+ }
+
+ uint32_t index;
+ nsINode* root = aKeepRootAlive ? aNode : nullptr;
+
+ if (nodeType == nsIDOMNode::DOCUMENT_NODE) {
+ index = txXPathNode::eDocument;
+ }
+ else {
+ index = txXPathNode::eContent;
+ if (root) {
+ root = txXPathNode::RootOf(root);
+ }
+ }
+
+ return new txXPathNode(aNode, index, root);
+}
+
+/* static */
+txXPathNode*
+txXPathNativeNode::createXPathNode(nsIDOMDocument* aDocument)
+{
+ nsCOMPtr<nsIDocument> document = do_QueryInterface(aDocument);
+ return new txXPathNode(document);
+}
+
+/* static */
+nsINode*
+txXPathNativeNode::getNode(const txXPathNode& aNode)
+{
+ if (!aNode.isAttribute()) {
+ return aNode.mNode;
+ }
+
+ const nsAttrName* name = aNode.Content()->GetAttrNameAt(aNode.mIndex);
+
+ nsAutoString namespaceURI;
+ nsContentUtils::NameSpaceManager()->GetNameSpaceURI(name->NamespaceID(), namespaceURI);
+
+ nsCOMPtr<Element> element = do_QueryInterface(aNode.mNode);
+ nsDOMAttributeMap* map = element->Attributes();
+ return map->GetNamedItemNS(namespaceURI,
+ nsDependentAtomString(name->LocalName()));
+}
+
+/* static */
+nsIContent*
+txXPathNativeNode::getContent(const txXPathNode& aNode)
+{
+ NS_ASSERTION(aNode.isContent(),
+ "Only call getContent on nsIContent wrappers!");
+ return aNode.Content();
+}
+
+/* static */
+nsIDocument*
+txXPathNativeNode::getDocument(const txXPathNode& aNode)
+{
+ NS_ASSERTION(aNode.isDocument(),
+ "Only call getDocument on nsIDocument wrappers!");
+ return aNode.Document();
+}