The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1041 lines
31KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. The code included in this file is provided under the terms of the ISC license
  8. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  9. To use, copy, modify, and/or distribute this software for any purpose with or
  10. without fee is hereby granted provided that the above copyright notice and
  11. this permission notice appear in all copies.
  12. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  13. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  14. DISCLAIMED.
  15. ==============================================================================
  16. */
  17. namespace juce
  18. {
  19. static bool isValidXmlNameStartCharacter (juce_wchar character) noexcept
  20. {
  21. return character == ':'
  22. || character == '_'
  23. || (character >= 'a' && character <= 'z')
  24. || (character >= 'A' && character <= 'Z')
  25. || (character >= 0xc0 && character <= 0xd6)
  26. || (character >= 0xd8 && character <= 0xf6)
  27. || (character >= 0xf8 && character <= 0x2ff)
  28. || (character >= 0x370 && character <= 0x37d)
  29. || (character >= 0x37f && character <= 0x1fff)
  30. || (character >= 0x200c && character <= 0x200d)
  31. || (character >= 0x2070 && character <= 0x218f)
  32. || (character >= 0x2c00 && character <= 0x2fef)
  33. || (character >= 0x3001 && character <= 0xd7ff)
  34. || (character >= 0xf900 && character <= 0xfdcf)
  35. || (character >= 0xfdf0 && character <= 0xfffd)
  36. || (character >= 0x10000 && character <= 0xeffff);
  37. }
  38. static bool isValidXmlNameBodyCharacter (juce_wchar character) noexcept
  39. {
  40. return isValidXmlNameStartCharacter (character)
  41. || character == '-'
  42. || character == '.'
  43. || character == 0xb7
  44. || (character >= '0' && character <= '9')
  45. || (character >= 0x300 && character <= 0x036f)
  46. || (character >= 0x203f && character <= 0x2040);
  47. }
  48. XmlElement::XmlAttributeNode::XmlAttributeNode (const XmlAttributeNode& other) noexcept
  49. : name (other.name),
  50. value (other.value)
  51. {
  52. }
  53. XmlElement::XmlAttributeNode::XmlAttributeNode (const Identifier& n, const String& v) noexcept
  54. : name (n), value (v)
  55. {
  56. jassert (isValidXmlName (name));
  57. }
  58. XmlElement::XmlAttributeNode::XmlAttributeNode (String::CharPointerType nameStart, String::CharPointerType nameEnd)
  59. : name (nameStart, nameEnd)
  60. {
  61. jassert (isValidXmlName (name));
  62. }
  63. //==============================================================================
  64. XmlElement::XmlElement (const String& tag)
  65. : tagName (StringPool::getGlobalPool().getPooledString (tag))
  66. {
  67. jassert (isValidXmlName (tagName));
  68. }
  69. XmlElement::XmlElement (const char* tag)
  70. : tagName (StringPool::getGlobalPool().getPooledString (tag))
  71. {
  72. jassert (isValidXmlName (tagName));
  73. }
  74. XmlElement::XmlElement (StringRef tag)
  75. : tagName (StringPool::getGlobalPool().getPooledString (tag))
  76. {
  77. jassert (isValidXmlName (tagName));
  78. }
  79. XmlElement::XmlElement (const Identifier& tag)
  80. : tagName (tag.toString())
  81. {
  82. jassert (isValidXmlName (tagName));
  83. }
  84. XmlElement::XmlElement (String::CharPointerType tagNameStart, String::CharPointerType tagNameEnd)
  85. : tagName (StringPool::getGlobalPool().getPooledString (tagNameStart, tagNameEnd))
  86. {
  87. jassert (isValidXmlName (tagName));
  88. }
  89. XmlElement::XmlElement (int /*dummy*/) noexcept
  90. {
  91. }
  92. XmlElement::XmlElement (const XmlElement& other)
  93. : tagName (other.tagName)
  94. {
  95. copyChildrenAndAttributesFrom (other);
  96. }
  97. XmlElement& XmlElement::operator= (const XmlElement& other)
  98. {
  99. if (this != &other)
  100. {
  101. removeAllAttributes();
  102. deleteAllChildElements();
  103. tagName = other.tagName;
  104. copyChildrenAndAttributesFrom (other);
  105. }
  106. return *this;
  107. }
  108. XmlElement::XmlElement (XmlElement&& other) noexcept
  109. : nextListItem (std::move (other.nextListItem)),
  110. firstChildElement (std::move (other.firstChildElement)),
  111. attributes (std::move (other.attributes)),
  112. tagName (std::move (other.tagName))
  113. {
  114. }
  115. XmlElement& XmlElement::operator= (XmlElement&& other) noexcept
  116. {
  117. jassert (this != &other); // hopefully the compiler should make this situation impossible!
  118. removeAllAttributes();
  119. deleteAllChildElements();
  120. nextListItem = std::move (other.nextListItem);
  121. firstChildElement = std::move (other.firstChildElement);
  122. attributes = std::move (other.attributes);
  123. tagName = std::move (other.tagName);
  124. return *this;
  125. }
  126. void XmlElement::copyChildrenAndAttributesFrom (const XmlElement& other)
  127. {
  128. jassert (firstChildElement.get() == nullptr);
  129. firstChildElement.addCopyOfList (other.firstChildElement);
  130. jassert (attributes.get() == nullptr);
  131. attributes.addCopyOfList (other.attributes);
  132. }
  133. XmlElement::~XmlElement() noexcept
  134. {
  135. firstChildElement.deleteAll();
  136. attributes.deleteAll();
  137. }
  138. //==============================================================================
  139. namespace XmlOutputFunctions
  140. {
  141. namespace LegalCharLookupTable
  142. {
  143. template <int c>
  144. struct Bit
  145. {
  146. enum { v = ((c >= 'a' && c <= 'z')
  147. || (c >= 'A' && c <= 'Z')
  148. || (c >= '0' && c <= '9')
  149. || c == ' ' || c == '.' || c == ',' || c == ';'
  150. || c == ':' || c == '-' || c == '(' || c == ')'
  151. || c == '_' || c == '+' || c == '=' || c == '?'
  152. || c == '!' || c == '$' || c == '#' || c == '@'
  153. || c == '[' || c == ']' || c == '/' || c == '|'
  154. || c == '*' || c == '%' || c == '~' || c == '{'
  155. || c == '}' || c == '\'' || c == '\\')
  156. ? (1 << (c & 7)) : 0 };
  157. };
  158. template <int tableIndex>
  159. struct Byte
  160. {
  161. enum { v = (int) Bit<tableIndex * 8 + 0>::v | (int) Bit<tableIndex * 8 + 1>::v
  162. | (int) Bit<tableIndex * 8 + 2>::v | (int) Bit<tableIndex * 8 + 3>::v
  163. | (int) Bit<tableIndex * 8 + 4>::v | (int) Bit<tableIndex * 8 + 5>::v
  164. | (int) Bit<tableIndex * 8 + 6>::v | (int) Bit<tableIndex * 8 + 7>::v };
  165. };
  166. static bool isLegal (uint32 c) noexcept
  167. {
  168. static const unsigned char legalChars[] = { Byte< 0>::v, Byte< 1>::v, Byte< 2>::v, Byte< 3>::v,
  169. Byte< 4>::v, Byte< 5>::v, Byte< 6>::v, Byte< 7>::v,
  170. Byte< 8>::v, Byte< 9>::v, Byte<10>::v, Byte<11>::v,
  171. Byte<12>::v, Byte<13>::v, Byte<14>::v, Byte<15>::v };
  172. return c < sizeof (legalChars) * 8
  173. && (legalChars[c >> 3] & (1 << (c & 7))) != 0;
  174. }
  175. }
  176. static void escapeIllegalXmlChars (OutputStream& outputStream, const String& text, bool changeNewLines)
  177. {
  178. auto t = text.getCharPointer();
  179. for (;;)
  180. {
  181. auto character = (uint32) t.getAndAdvance();
  182. if (character == 0)
  183. break;
  184. if (LegalCharLookupTable::isLegal (character))
  185. {
  186. outputStream << (char) character;
  187. }
  188. else
  189. {
  190. switch (character)
  191. {
  192. case '&': outputStream << "&amp;"; break;
  193. case '"': outputStream << "&quot;"; break;
  194. case '>': outputStream << "&gt;"; break;
  195. case '<': outputStream << "&lt;"; break;
  196. case '\n':
  197. case '\r':
  198. if (! changeNewLines)
  199. {
  200. outputStream << (char) character;
  201. break;
  202. }
  203. JUCE_FALLTHROUGH
  204. default:
  205. outputStream << "&#" << ((int) character) << ';';
  206. break;
  207. }
  208. }
  209. }
  210. }
  211. static void writeSpaces (OutputStream& out, const size_t numSpaces)
  212. {
  213. out.writeRepeatedByte (' ', numSpaces);
  214. }
  215. }
  216. void XmlElement::writeElementAsText (OutputStream& outputStream,
  217. int indentationLevel,
  218. int lineWrapLength,
  219. const char* newLineChars) const
  220. {
  221. if (indentationLevel >= 0)
  222. XmlOutputFunctions::writeSpaces (outputStream, (size_t) indentationLevel);
  223. if (! isTextElement())
  224. {
  225. outputStream.writeByte ('<');
  226. outputStream << tagName;
  227. {
  228. auto attIndent = (size_t) (indentationLevel + tagName.length() + 1);
  229. int lineLen = 0;
  230. for (auto* att = attributes.get(); att != nullptr; att = att->nextListItem)
  231. {
  232. if (lineLen > lineWrapLength && indentationLevel >= 0)
  233. {
  234. outputStream << newLineChars;
  235. XmlOutputFunctions::writeSpaces (outputStream, attIndent);
  236. lineLen = 0;
  237. }
  238. auto startPos = outputStream.getPosition();
  239. outputStream.writeByte (' ');
  240. outputStream << att->name;
  241. outputStream.write ("=\"", 2);
  242. XmlOutputFunctions::escapeIllegalXmlChars (outputStream, att->value, true);
  243. outputStream.writeByte ('"');
  244. lineLen += (int) (outputStream.getPosition() - startPos);
  245. }
  246. }
  247. if (auto* child = firstChildElement.get())
  248. {
  249. outputStream.writeByte ('>');
  250. bool lastWasTextNode = false;
  251. for (; child != nullptr; child = child->nextListItem)
  252. {
  253. if (child->isTextElement())
  254. {
  255. XmlOutputFunctions::escapeIllegalXmlChars (outputStream, child->getText(), false);
  256. lastWasTextNode = true;
  257. }
  258. else
  259. {
  260. if (indentationLevel >= 0 && ! lastWasTextNode)
  261. outputStream << newLineChars;
  262. child->writeElementAsText (outputStream,
  263. lastWasTextNode ? 0 : (indentationLevel + (indentationLevel >= 0 ? 2 : 0)), lineWrapLength,
  264. newLineChars);
  265. lastWasTextNode = false;
  266. }
  267. }
  268. if (indentationLevel >= 0 && ! lastWasTextNode)
  269. {
  270. outputStream << newLineChars;
  271. XmlOutputFunctions::writeSpaces (outputStream, (size_t) indentationLevel);
  272. }
  273. outputStream.write ("</", 2);
  274. outputStream << tagName;
  275. outputStream.writeByte ('>');
  276. }
  277. else
  278. {
  279. outputStream.write ("/>", 2);
  280. }
  281. }
  282. else
  283. {
  284. XmlOutputFunctions::escapeIllegalXmlChars (outputStream, getText(), false);
  285. }
  286. }
  287. XmlElement::TextFormat::TextFormat() {}
  288. XmlElement::TextFormat XmlElement::TextFormat::singleLine() const
  289. {
  290. auto f = *this;
  291. f.newLineChars = nullptr;
  292. return f;
  293. }
  294. XmlElement::TextFormat XmlElement::TextFormat::withoutHeader() const
  295. {
  296. auto f = *this;
  297. f.addDefaultHeader = false;
  298. return f;
  299. }
  300. String XmlElement::toString (const TextFormat& options) const
  301. {
  302. MemoryOutputStream mem (2048);
  303. writeTo (mem, options);
  304. return mem.toUTF8();
  305. }
  306. void XmlElement::writeTo (OutputStream& output, const TextFormat& options) const
  307. {
  308. if (options.customHeader.isNotEmpty())
  309. {
  310. output << options.customHeader;
  311. if (options.newLineChars == nullptr)
  312. output.writeByte (' ');
  313. else
  314. output << options.newLineChars
  315. << options.newLineChars;
  316. }
  317. else if (options.addDefaultHeader)
  318. {
  319. output << "<?xml version=\"1.0\" encoding=\"";
  320. if (options.customEncoding.isNotEmpty())
  321. output << options.customEncoding;
  322. else
  323. output << "UTF-8";
  324. output << "\"?>";
  325. if (options.newLineChars == nullptr)
  326. output.writeByte (' ');
  327. else
  328. output << options.newLineChars
  329. << options.newLineChars;
  330. }
  331. if (options.dtd.isNotEmpty())
  332. {
  333. output << options.dtd;
  334. if (options.newLineChars == nullptr)
  335. output.writeByte (' ');
  336. else
  337. output << options.newLineChars;
  338. }
  339. writeElementAsText (output, options.newLineChars == nullptr ? -1 : 0,
  340. options.lineWrapLength,
  341. options.newLineChars);
  342. if (options.newLineChars != nullptr)
  343. output << options.newLineChars;
  344. }
  345. bool XmlElement::writeTo (const File& destinationFile, const TextFormat& options) const
  346. {
  347. TemporaryFile tempFile (destinationFile);
  348. {
  349. FileOutputStream out (tempFile.getFile());
  350. if (! out.openedOk())
  351. return false;
  352. writeTo (out, options);
  353. out.flush(); // (called explicitly to force an fsync on posix)
  354. if (out.getStatus().failed())
  355. return false;
  356. }
  357. return tempFile.overwriteTargetFileWithTemporary();
  358. }
  359. String XmlElement::createDocument (StringRef dtdToUse, bool allOnOneLine, bool includeXmlHeader,
  360. StringRef encodingType, int lineWrapLength) const
  361. {
  362. TextFormat options;
  363. options.dtd = dtdToUse;
  364. options.customEncoding = encodingType;
  365. options.addDefaultHeader = includeXmlHeader;
  366. options.lineWrapLength = lineWrapLength;
  367. if (allOnOneLine)
  368. options.newLineChars = nullptr;
  369. return toString (options);
  370. }
  371. void XmlElement::writeToStream (OutputStream& output, StringRef dtdToUse,
  372. bool allOnOneLine, bool includeXmlHeader,
  373. StringRef encodingType, int lineWrapLength) const
  374. {
  375. TextFormat options;
  376. options.dtd = dtdToUse;
  377. options.customEncoding = encodingType;
  378. options.addDefaultHeader = includeXmlHeader;
  379. options.lineWrapLength = lineWrapLength;
  380. if (allOnOneLine)
  381. options.newLineChars = nullptr;
  382. writeTo (output, options);
  383. }
  384. bool XmlElement::writeToFile (const File& file, StringRef dtdToUse,
  385. StringRef encodingType, int lineWrapLength) const
  386. {
  387. TextFormat options;
  388. options.dtd = dtdToUse;
  389. options.customEncoding = encodingType;
  390. options.lineWrapLength = lineWrapLength;
  391. return writeTo (file, options);
  392. }
  393. //==============================================================================
  394. bool XmlElement::hasTagName (StringRef possibleTagName) const noexcept
  395. {
  396. const bool matches = tagName.equalsIgnoreCase (possibleTagName);
  397. // XML tags should be case-sensitive, so although this method allows a
  398. // case-insensitive match to pass, you should try to avoid this.
  399. jassert ((! matches) || tagName == possibleTagName);
  400. return matches;
  401. }
  402. String XmlElement::getNamespace() const
  403. {
  404. return tagName.upToFirstOccurrenceOf (":", false, false);
  405. }
  406. String XmlElement::getTagNameWithoutNamespace() const
  407. {
  408. return tagName.fromLastOccurrenceOf (":", false, false);
  409. }
  410. bool XmlElement::hasTagNameIgnoringNamespace (StringRef possibleTagName) const
  411. {
  412. return hasTagName (possibleTagName) || getTagNameWithoutNamespace() == possibleTagName;
  413. }
  414. XmlElement* XmlElement::getNextElementWithTagName (StringRef requiredTagName) const
  415. {
  416. auto* e = nextListItem.get();
  417. while (e != nullptr && ! e->hasTagName (requiredTagName))
  418. e = e->nextListItem;
  419. return e;
  420. }
  421. void XmlElement::setTagName (StringRef newTagName)
  422. {
  423. jassert (isValidXmlName (newTagName));
  424. tagName = StringPool::getGlobalPool().getPooledString (newTagName);
  425. }
  426. //==============================================================================
  427. int XmlElement::getNumAttributes() const noexcept
  428. {
  429. return attributes.size();
  430. }
  431. static const String& getEmptyStringRef() noexcept
  432. {
  433. static String empty;
  434. return empty;
  435. }
  436. const String& XmlElement::getAttributeName (const int index) const noexcept
  437. {
  438. if (auto* att = attributes[index].get())
  439. return att->name.toString();
  440. return getEmptyStringRef();
  441. }
  442. const String& XmlElement::getAttributeValue (const int index) const noexcept
  443. {
  444. if (auto* att = attributes[index].get())
  445. return att->value;
  446. return getEmptyStringRef();
  447. }
  448. XmlElement::XmlAttributeNode* XmlElement::getAttribute (StringRef attributeName) const noexcept
  449. {
  450. for (auto* att = attributes.get(); att != nullptr; att = att->nextListItem)
  451. if (att->name == attributeName)
  452. return att;
  453. return nullptr;
  454. }
  455. bool XmlElement::hasAttribute (StringRef attributeName) const noexcept
  456. {
  457. return getAttribute (attributeName) != nullptr;
  458. }
  459. //==============================================================================
  460. const String& XmlElement::getStringAttribute (StringRef attributeName) const noexcept
  461. {
  462. if (auto* att = getAttribute (attributeName))
  463. return att->value;
  464. return getEmptyStringRef();
  465. }
  466. String XmlElement::getStringAttribute (StringRef attributeName, const String& defaultReturnValue) const
  467. {
  468. if (auto* att = getAttribute (attributeName))
  469. return att->value;
  470. return defaultReturnValue;
  471. }
  472. int XmlElement::getIntAttribute (StringRef attributeName, const int defaultReturnValue) const
  473. {
  474. if (auto* att = getAttribute (attributeName))
  475. return att->value.getIntValue();
  476. return defaultReturnValue;
  477. }
  478. double XmlElement::getDoubleAttribute (StringRef attributeName, const double defaultReturnValue) const
  479. {
  480. if (auto* att = getAttribute (attributeName))
  481. return att->value.getDoubleValue();
  482. return defaultReturnValue;
  483. }
  484. bool XmlElement::getBoolAttribute (StringRef attributeName, const bool defaultReturnValue) const
  485. {
  486. if (auto* att = getAttribute (attributeName))
  487. {
  488. auto firstChar = *(att->value.getCharPointer().findEndOfWhitespace());
  489. return firstChar == '1'
  490. || firstChar == 't'
  491. || firstChar == 'y'
  492. || firstChar == 'T'
  493. || firstChar == 'Y';
  494. }
  495. return defaultReturnValue;
  496. }
  497. bool XmlElement::compareAttribute (StringRef attributeName,
  498. StringRef stringToCompareAgainst,
  499. const bool ignoreCase) const noexcept
  500. {
  501. if (auto* att = getAttribute (attributeName))
  502. return ignoreCase ? att->value.equalsIgnoreCase (stringToCompareAgainst)
  503. : att->value == stringToCompareAgainst;
  504. return false;
  505. }
  506. //==============================================================================
  507. void XmlElement::setAttribute (const Identifier& attributeName, const String& value)
  508. {
  509. if (attributes == nullptr)
  510. {
  511. attributes = new XmlAttributeNode (attributeName, value);
  512. }
  513. else
  514. {
  515. for (auto* att = attributes.get(); ; att = att->nextListItem)
  516. {
  517. if (att->name == attributeName)
  518. {
  519. att->value = value;
  520. break;
  521. }
  522. if (att->nextListItem == nullptr)
  523. {
  524. att->nextListItem = new XmlAttributeNode (attributeName, value);
  525. break;
  526. }
  527. }
  528. }
  529. }
  530. void XmlElement::setAttribute (const Identifier& attributeName, const int number)
  531. {
  532. setAttribute (attributeName, String (number));
  533. }
  534. void XmlElement::setAttribute (const Identifier& attributeName, const double number)
  535. {
  536. setAttribute (attributeName, serialiseDouble (number));
  537. }
  538. void XmlElement::removeAttribute (const Identifier& attributeName) noexcept
  539. {
  540. for (auto* att = &attributes; att->get() != nullptr; att = &(att->get()->nextListItem))
  541. {
  542. if (att->get()->name == attributeName)
  543. {
  544. delete att->removeNext();
  545. break;
  546. }
  547. }
  548. }
  549. void XmlElement::removeAllAttributes() noexcept
  550. {
  551. attributes.deleteAll();
  552. }
  553. //==============================================================================
  554. int XmlElement::getNumChildElements() const noexcept
  555. {
  556. return firstChildElement.size();
  557. }
  558. XmlElement* XmlElement::getChildElement (const int index) const noexcept
  559. {
  560. return firstChildElement[index].get();
  561. }
  562. XmlElement* XmlElement::getChildByName (StringRef childName) const noexcept
  563. {
  564. jassert (! childName.isEmpty());
  565. for (auto* child = firstChildElement.get(); child != nullptr; child = child->nextListItem)
  566. if (child->hasTagName (childName))
  567. return child;
  568. return nullptr;
  569. }
  570. XmlElement* XmlElement::getChildByAttribute (StringRef attributeName, StringRef attributeValue) const noexcept
  571. {
  572. jassert (! attributeName.isEmpty());
  573. for (auto* child = firstChildElement.get(); child != nullptr; child = child->nextListItem)
  574. if (child->compareAttribute (attributeName, attributeValue))
  575. return child;
  576. return nullptr;
  577. }
  578. void XmlElement::addChildElement (XmlElement* const newNode) noexcept
  579. {
  580. if (newNode != nullptr)
  581. {
  582. // The element being added must not be a child of another node!
  583. jassert (newNode->nextListItem == nullptr);
  584. firstChildElement.append (newNode);
  585. }
  586. }
  587. void XmlElement::insertChildElement (XmlElement* const newNode, int indexToInsertAt) noexcept
  588. {
  589. if (newNode != nullptr)
  590. {
  591. // The element being added must not be a child of another node!
  592. jassert (newNode->nextListItem == nullptr);
  593. firstChildElement.insertAtIndex (indexToInsertAt, newNode);
  594. }
  595. }
  596. void XmlElement::prependChildElement (XmlElement* newNode) noexcept
  597. {
  598. if (newNode != nullptr)
  599. {
  600. // The element being added must not be a child of another node!
  601. jassert (newNode->nextListItem == nullptr);
  602. firstChildElement.insertNext (newNode);
  603. }
  604. }
  605. XmlElement* XmlElement::createNewChildElement (StringRef childTagName)
  606. {
  607. auto newElement = new XmlElement (childTagName);
  608. addChildElement (newElement);
  609. return newElement;
  610. }
  611. bool XmlElement::replaceChildElement (XmlElement* const currentChildElement,
  612. XmlElement* const newNode) noexcept
  613. {
  614. if (newNode != nullptr)
  615. {
  616. if (auto* p = firstChildElement.findPointerTo (currentChildElement))
  617. {
  618. if (currentChildElement != newNode)
  619. delete p->replaceNext (newNode);
  620. return true;
  621. }
  622. }
  623. return false;
  624. }
  625. void XmlElement::removeChildElement (XmlElement* const childToRemove,
  626. const bool shouldDeleteTheChild) noexcept
  627. {
  628. if (childToRemove != nullptr)
  629. {
  630. jassert (containsChildElement (childToRemove));
  631. firstChildElement.remove (childToRemove);
  632. if (shouldDeleteTheChild)
  633. delete childToRemove;
  634. }
  635. }
  636. bool XmlElement::isEquivalentTo (const XmlElement* const other,
  637. const bool ignoreOrderOfAttributes) const noexcept
  638. {
  639. if (this != other)
  640. {
  641. if (other == nullptr || tagName != other->tagName)
  642. return false;
  643. if (ignoreOrderOfAttributes)
  644. {
  645. int totalAtts = 0;
  646. for (auto* att = attributes.get(); att != nullptr; att = att->nextListItem)
  647. {
  648. if (! other->compareAttribute (att->name, att->value))
  649. return false;
  650. ++totalAtts;
  651. }
  652. if (totalAtts != other->getNumAttributes())
  653. return false;
  654. }
  655. else
  656. {
  657. auto* thisAtt = attributes.get();
  658. auto* otherAtt = other->attributes.get();
  659. for (;;)
  660. {
  661. if (thisAtt == nullptr || otherAtt == nullptr)
  662. {
  663. if (thisAtt == otherAtt) // both nullptr, so it's a match
  664. break;
  665. return false;
  666. }
  667. if (thisAtt->name != otherAtt->name
  668. || thisAtt->value != otherAtt->value)
  669. {
  670. return false;
  671. }
  672. thisAtt = thisAtt->nextListItem;
  673. otherAtt = otherAtt->nextListItem;
  674. }
  675. }
  676. auto* thisChild = firstChildElement.get();
  677. auto* otherChild = other->firstChildElement.get();
  678. for (;;)
  679. {
  680. if (thisChild == nullptr || otherChild == nullptr)
  681. {
  682. if (thisChild == otherChild) // both 0, so it's a match
  683. break;
  684. return false;
  685. }
  686. if (! thisChild->isEquivalentTo (otherChild, ignoreOrderOfAttributes))
  687. return false;
  688. thisChild = thisChild->nextListItem;
  689. otherChild = otherChild->nextListItem;
  690. }
  691. }
  692. return true;
  693. }
  694. void XmlElement::deleteAllChildElements() noexcept
  695. {
  696. firstChildElement.deleteAll();
  697. }
  698. void XmlElement::deleteAllChildElementsWithTagName (StringRef name) noexcept
  699. {
  700. for (auto* child = firstChildElement.get(); child != nullptr;)
  701. {
  702. auto* nextChild = child->nextListItem.get();
  703. if (child->hasTagName (name))
  704. removeChildElement (child, true);
  705. child = nextChild;
  706. }
  707. }
  708. bool XmlElement::containsChildElement (const XmlElement* const possibleChild) const noexcept
  709. {
  710. return firstChildElement.contains (possibleChild);
  711. }
  712. XmlElement* XmlElement::findParentElementOf (const XmlElement* const elementToLookFor) noexcept
  713. {
  714. if (this == elementToLookFor || elementToLookFor == nullptr)
  715. return nullptr;
  716. for (auto* child = firstChildElement.get(); child != nullptr; child = child->nextListItem)
  717. {
  718. if (elementToLookFor == child)
  719. return this;
  720. if (auto* found = child->findParentElementOf (elementToLookFor))
  721. return found;
  722. }
  723. return nullptr;
  724. }
  725. void XmlElement::getChildElementsAsArray (XmlElement** elems) const noexcept
  726. {
  727. firstChildElement.copyToArray (elems);
  728. }
  729. void XmlElement::reorderChildElements (XmlElement** elems, int num) noexcept
  730. {
  731. auto* e = elems[0];
  732. firstChildElement = e;
  733. for (int i = 1; i < num; ++i)
  734. {
  735. e->nextListItem = elems[i];
  736. e = e->nextListItem;
  737. }
  738. e->nextListItem = nullptr;
  739. }
  740. //==============================================================================
  741. bool XmlElement::isTextElement() const noexcept
  742. {
  743. return tagName.isEmpty();
  744. }
  745. static const String juce_xmltextContentAttributeName ("text");
  746. const String& XmlElement::getText() const noexcept
  747. {
  748. jassert (isTextElement()); // you're trying to get the text from an element that
  749. // isn't actually a text element.. If this contains text sub-nodes, you
  750. // probably want to use getAllSubText instead.
  751. return getStringAttribute (juce_xmltextContentAttributeName);
  752. }
  753. void XmlElement::setText (const String& newText)
  754. {
  755. if (isTextElement())
  756. setAttribute (juce_xmltextContentAttributeName, newText);
  757. else
  758. jassertfalse; // you can only change the text in a text element, not a normal one.
  759. }
  760. String XmlElement::getAllSubText() const
  761. {
  762. if (isTextElement())
  763. return getText();
  764. if (getNumChildElements() == 1)
  765. return firstChildElement.get()->getAllSubText();
  766. MemoryOutputStream mem (1024);
  767. for (auto* child = firstChildElement.get(); child != nullptr; child = child->nextListItem)
  768. mem << child->getAllSubText();
  769. return mem.toUTF8();
  770. }
  771. String XmlElement::getChildElementAllSubText (StringRef childTagName, const String& defaultReturnValue) const
  772. {
  773. if (auto* child = getChildByName (childTagName))
  774. return child->getAllSubText();
  775. return defaultReturnValue;
  776. }
  777. XmlElement* XmlElement::createTextElement (const String& text)
  778. {
  779. auto e = new XmlElement ((int) 0);
  780. e->setAttribute (juce_xmltextContentAttributeName, text);
  781. return e;
  782. }
  783. bool XmlElement::isValidXmlName (StringRef text) noexcept
  784. {
  785. if (text.isEmpty() || ! isValidXmlNameStartCharacter (text.text.getAndAdvance()))
  786. return false;
  787. for (;;)
  788. {
  789. if (text.isEmpty())
  790. return true;
  791. if (! isValidXmlNameBodyCharacter (text.text.getAndAdvance()))
  792. return false;
  793. }
  794. }
  795. void XmlElement::addTextElement (const String& text)
  796. {
  797. addChildElement (createTextElement (text));
  798. }
  799. void XmlElement::deleteAllTextElements() noexcept
  800. {
  801. for (auto* child = firstChildElement.get(); child != nullptr;)
  802. {
  803. auto* next = child->nextListItem.get();
  804. if (child->isTextElement())
  805. removeChildElement (child, true);
  806. child = next;
  807. }
  808. }
  809. //==============================================================================
  810. //==============================================================================
  811. #if JUCE_UNIT_TESTS
  812. class XmlElementTests final : public UnitTest
  813. {
  814. public:
  815. XmlElementTests()
  816. : UnitTest ("XmlElement", UnitTestCategories::xml)
  817. {}
  818. void runTest() override
  819. {
  820. {
  821. beginTest ("Float formatting");
  822. auto element = std::make_unique<XmlElement> ("test");
  823. Identifier number ("number");
  824. std::map<double, String> tests;
  825. tests[1] = "1.0";
  826. tests[1.1] = "1.1";
  827. tests[1.01] = "1.01";
  828. tests[0.76378] = "0.76378";
  829. tests[-10] = "-10.0";
  830. tests[10.01] = "10.01";
  831. tests[0.0123] = "0.0123";
  832. tests[-3.7e-27] = "-3.7e-27";
  833. tests[1e+40] = "1.0e40";
  834. tests[-12345678901234567.0] = "-1.234567890123457e16";
  835. tests[192000] = "192000.0";
  836. tests[1234567] = "1.234567e6";
  837. tests[0.00006] = "0.00006";
  838. tests[0.000006] = "6.0e-6";
  839. for (auto& test : tests)
  840. {
  841. element->setAttribute (number, test.first);
  842. expectEquals (element->getStringAttribute (number), test.second);
  843. }
  844. }
  845. }
  846. };
  847. static XmlElementTests xmlElementTests;
  848. #endif
  849. } // namespace juce