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.

904 lines
26KB

  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. XmlDocument::XmlDocument (const String& text) : originalText (text) {}
  20. XmlDocument::XmlDocument (const File& file) : inputSource (new FileInputSource (file)) {}
  21. XmlDocument::~XmlDocument() {}
  22. std::unique_ptr<XmlElement> XmlDocument::parse (const File& file)
  23. {
  24. return XmlDocument (file).getDocumentElement();
  25. }
  26. std::unique_ptr<XmlElement> XmlDocument::parse (const String& textToParse)
  27. {
  28. return XmlDocument (textToParse).getDocumentElement();
  29. }
  30. std::unique_ptr<XmlElement> parseXML (const String& textToParse)
  31. {
  32. return XmlDocument (textToParse).getDocumentElement();
  33. }
  34. std::unique_ptr<XmlElement> parseXML (const File& file)
  35. {
  36. return XmlDocument (file).getDocumentElement();
  37. }
  38. std::unique_ptr<XmlElement> parseXMLIfTagMatches (const String& textToParse, StringRef requiredTag)
  39. {
  40. return XmlDocument (textToParse).getDocumentElementIfTagMatches (requiredTag);
  41. }
  42. std::unique_ptr<XmlElement> parseXMLIfTagMatches (const File& file, StringRef requiredTag)
  43. {
  44. return XmlDocument (file).getDocumentElementIfTagMatches (requiredTag);
  45. }
  46. void XmlDocument::setInputSource (InputSource* newSource) noexcept
  47. {
  48. inputSource.reset (newSource);
  49. }
  50. void XmlDocument::setEmptyTextElementsIgnored (bool shouldBeIgnored) noexcept
  51. {
  52. ignoreEmptyTextElements = shouldBeIgnored;
  53. }
  54. namespace XmlIdentifierChars
  55. {
  56. static bool isIdentifierCharSlow (juce_wchar c) noexcept
  57. {
  58. return CharacterFunctions::isLetterOrDigit (c)
  59. || c == '_' || c == '-' || c == ':' || c == '.';
  60. }
  61. static bool isIdentifierChar (juce_wchar c) noexcept
  62. {
  63. static const uint32 legalChars[] = { 0, 0x7ff6000, 0x87fffffe, 0x7fffffe, 0 };
  64. return ((int) c < (int) numElementsInArray (legalChars) * 32) ? ((legalChars [c >> 5] & (uint32) (1 << (c & 31))) != 0)
  65. : isIdentifierCharSlow (c);
  66. }
  67. /*static void generateIdentifierCharConstants()
  68. {
  69. uint32 n[8] = { 0 };
  70. for (int i = 0; i < 256; ++i)
  71. if (isIdentifierCharSlow (i))
  72. n[i >> 5] |= (1 << (i & 31));
  73. String s;
  74. for (int i = 0; i < 8; ++i)
  75. s << "0x" << String::toHexString ((int) n[i]) << ", ";
  76. DBG (s);
  77. }*/
  78. static String::CharPointerType findEndOfToken (String::CharPointerType p) noexcept
  79. {
  80. while (isIdentifierChar (*p))
  81. ++p;
  82. return p;
  83. }
  84. }
  85. std::unique_ptr<XmlElement> XmlDocument::getDocumentElement (const bool onlyReadOuterDocumentElement)
  86. {
  87. if (originalText.isEmpty() && inputSource != nullptr)
  88. {
  89. std::unique_ptr<InputStream> in (inputSource->createInputStream());
  90. if (in != nullptr)
  91. {
  92. MemoryOutputStream data;
  93. data.writeFromInputStream (*in, onlyReadOuterDocumentElement ? 8192 : -1);
  94. #if JUCE_STRING_UTF_TYPE == 8
  95. if (data.getDataSize() > 2)
  96. {
  97. data.writeByte (0);
  98. auto* text = static_cast<const char*> (data.getData());
  99. if (CharPointer_UTF16::isByteOrderMarkBigEndian (text)
  100. || CharPointer_UTF16::isByteOrderMarkLittleEndian (text))
  101. {
  102. originalText = data.toString();
  103. }
  104. else
  105. {
  106. if (CharPointer_UTF8::isByteOrderMark (text))
  107. text += 3;
  108. // parse the input buffer directly to avoid copying it all to a string..
  109. return parseDocumentElement (String::CharPointerType (text), onlyReadOuterDocumentElement);
  110. }
  111. }
  112. #else
  113. originalText = data.toString();
  114. #endif
  115. }
  116. }
  117. return parseDocumentElement (originalText.getCharPointer(), onlyReadOuterDocumentElement);
  118. }
  119. std::unique_ptr<XmlElement> XmlDocument::getDocumentElementIfTagMatches (StringRef requiredTag)
  120. {
  121. if (auto xml = getDocumentElement (true))
  122. if (xml->hasTagName (requiredTag))
  123. return getDocumentElement (false);
  124. return {};
  125. }
  126. const String& XmlDocument::getLastParseError() const noexcept
  127. {
  128. return lastError;
  129. }
  130. void XmlDocument::setLastError (const String& desc, const bool carryOn)
  131. {
  132. lastError = desc;
  133. errorOccurred = ! carryOn;
  134. }
  135. String XmlDocument::getFileContents (const String& filename) const
  136. {
  137. if (inputSource != nullptr)
  138. {
  139. std::unique_ptr<InputStream> in (inputSource->createInputStreamFor (filename.trim().unquoted()));
  140. if (in != nullptr)
  141. return in->readEntireStreamAsString();
  142. }
  143. return {};
  144. }
  145. juce_wchar XmlDocument::readNextChar() noexcept
  146. {
  147. auto c = input.getAndAdvance();
  148. if (c == 0)
  149. {
  150. outOfData = true;
  151. --input;
  152. }
  153. return c;
  154. }
  155. std::unique_ptr<XmlElement> XmlDocument::parseDocumentElement (String::CharPointerType textToParse,
  156. bool onlyReadOuterDocumentElement)
  157. {
  158. input = textToParse;
  159. errorOccurred = false;
  160. outOfData = false;
  161. needToLoadDTD = true;
  162. if (textToParse.isEmpty())
  163. {
  164. lastError = "not enough input";
  165. }
  166. else if (! parseHeader())
  167. {
  168. lastError = "malformed header";
  169. }
  170. else if (! parseDTD())
  171. {
  172. lastError = "malformed DTD";
  173. }
  174. else
  175. {
  176. lastError.clear();
  177. std::unique_ptr<XmlElement> result (readNextElement (! onlyReadOuterDocumentElement));
  178. if (! errorOccurred)
  179. return result;
  180. }
  181. return {};
  182. }
  183. bool XmlDocument::parseHeader()
  184. {
  185. skipNextWhiteSpace();
  186. if (CharacterFunctions::compareUpTo (input, CharPointer_ASCII ("<?xml"), 5) == 0)
  187. {
  188. auto headerEnd = CharacterFunctions::find (input, CharPointer_ASCII ("?>"));
  189. if (headerEnd.isEmpty())
  190. return false;
  191. #if JUCE_DEBUG
  192. auto encoding = String (input, headerEnd)
  193. .fromFirstOccurrenceOf ("encoding", false, true)
  194. .fromFirstOccurrenceOf ("=", false, false)
  195. .fromFirstOccurrenceOf ("\"", false, false)
  196. .upToFirstOccurrenceOf ("\"", false, false)
  197. .trim();
  198. /* If you load an XML document with a non-UTF encoding type, it may have been
  199. loaded wrongly.. Since all the files are read via the normal juce file streams,
  200. they're treated as UTF-8, so by the time it gets to the parser, the encoding will
  201. have been lost. Best plan is to stick to utf-8 or if you have specific files to
  202. read, use your own code to convert them to a unicode String, and pass that to the
  203. XML parser.
  204. */
  205. jassert (encoding.isEmpty() || encoding.startsWithIgnoreCase ("utf-"));
  206. #endif
  207. input = headerEnd + 2;
  208. skipNextWhiteSpace();
  209. }
  210. return true;
  211. }
  212. bool XmlDocument::parseDTD()
  213. {
  214. if (CharacterFunctions::compareUpTo (input, CharPointer_ASCII ("<!DOCTYPE"), 9) == 0)
  215. {
  216. input += 9;
  217. auto dtdStart = input;
  218. for (int n = 1; n > 0;)
  219. {
  220. auto c = readNextChar();
  221. if (outOfData)
  222. return false;
  223. if (c == '<')
  224. ++n;
  225. else if (c == '>')
  226. --n;
  227. }
  228. dtdText = String (dtdStart, input - 1).trim();
  229. }
  230. return true;
  231. }
  232. void XmlDocument::skipNextWhiteSpace()
  233. {
  234. for (;;)
  235. {
  236. input.incrementToEndOfWhitespace();
  237. if (input.isEmpty())
  238. {
  239. outOfData = true;
  240. break;
  241. }
  242. if (*input == '<')
  243. {
  244. if (input[1] == '!'
  245. && input[2] == '-'
  246. && input[3] == '-')
  247. {
  248. input += 4;
  249. auto closeComment = input.indexOf (CharPointer_ASCII ("-->"));
  250. if (closeComment < 0)
  251. {
  252. outOfData = true;
  253. break;
  254. }
  255. input += closeComment + 3;
  256. continue;
  257. }
  258. if (input[1] == '?')
  259. {
  260. input += 2;
  261. auto closeBracket = input.indexOf (CharPointer_ASCII ("?>"));
  262. if (closeBracket < 0)
  263. {
  264. outOfData = true;
  265. break;
  266. }
  267. input += closeBracket + 2;
  268. continue;
  269. }
  270. }
  271. break;
  272. }
  273. }
  274. void XmlDocument::readQuotedString (String& result)
  275. {
  276. auto quote = readNextChar();
  277. while (! outOfData)
  278. {
  279. auto c = readNextChar();
  280. if (c == quote)
  281. break;
  282. --input;
  283. if (c == '&')
  284. {
  285. readEntity (result);
  286. }
  287. else
  288. {
  289. auto start = input;
  290. for (;;)
  291. {
  292. auto character = *input;
  293. if (character == quote)
  294. {
  295. result.appendCharPointer (start, input);
  296. ++input;
  297. return;
  298. }
  299. if (character == '&')
  300. {
  301. result.appendCharPointer (start, input);
  302. break;
  303. }
  304. if (character == 0)
  305. {
  306. setLastError ("unmatched quotes", false);
  307. outOfData = true;
  308. break;
  309. }
  310. ++input;
  311. }
  312. }
  313. }
  314. }
  315. XmlElement* XmlDocument::readNextElement (const bool alsoParseSubElements)
  316. {
  317. XmlElement* node = nullptr;
  318. skipNextWhiteSpace();
  319. if (outOfData)
  320. return nullptr;
  321. if (*input == '<')
  322. {
  323. ++input;
  324. auto endOfToken = XmlIdentifierChars::findEndOfToken (input);
  325. if (endOfToken == input)
  326. {
  327. // no tag name - but allow for a gap after the '<' before giving an error
  328. skipNextWhiteSpace();
  329. endOfToken = XmlIdentifierChars::findEndOfToken (input);
  330. if (endOfToken == input)
  331. {
  332. setLastError ("tag name missing", false);
  333. return node;
  334. }
  335. }
  336. node = new XmlElement (input, endOfToken);
  337. input = endOfToken;
  338. LinkedListPointer<XmlElement::XmlAttributeNode>::Appender attributeAppender (node->attributes);
  339. // look for attributes
  340. for (;;)
  341. {
  342. skipNextWhiteSpace();
  343. auto c = *input;
  344. // empty tag..
  345. if (c == '/' && input[1] == '>')
  346. {
  347. input += 2;
  348. break;
  349. }
  350. // parse the guts of the element..
  351. if (c == '>')
  352. {
  353. ++input;
  354. if (alsoParseSubElements)
  355. readChildElements (*node);
  356. break;
  357. }
  358. // get an attribute..
  359. if (XmlIdentifierChars::isIdentifierChar (c))
  360. {
  361. auto attNameEnd = XmlIdentifierChars::findEndOfToken (input);
  362. if (attNameEnd != input)
  363. {
  364. auto attNameStart = input;
  365. input = attNameEnd;
  366. skipNextWhiteSpace();
  367. if (readNextChar() == '=')
  368. {
  369. skipNextWhiteSpace();
  370. auto nextChar = *input;
  371. if (nextChar == '"' || nextChar == '\'')
  372. {
  373. auto* newAtt = new XmlElement::XmlAttributeNode (attNameStart, attNameEnd);
  374. readQuotedString (newAtt->value);
  375. attributeAppender.append (newAtt);
  376. continue;
  377. }
  378. }
  379. else
  380. {
  381. setLastError ("expected '=' after attribute '"
  382. + String (attNameStart, attNameEnd) + "'", false);
  383. return node;
  384. }
  385. }
  386. }
  387. else
  388. {
  389. if (! outOfData)
  390. setLastError ("illegal character found in " + node->getTagName() + ": '" + c + "'", false);
  391. }
  392. break;
  393. }
  394. }
  395. return node;
  396. }
  397. void XmlDocument::readChildElements (XmlElement& parent)
  398. {
  399. LinkedListPointer<XmlElement>::Appender childAppender (parent.firstChildElement);
  400. for (;;)
  401. {
  402. auto preWhitespaceInput = input;
  403. skipNextWhiteSpace();
  404. if (outOfData)
  405. {
  406. setLastError ("unmatched tags", false);
  407. break;
  408. }
  409. if (*input == '<')
  410. {
  411. auto c1 = input[1];
  412. if (c1 == '/')
  413. {
  414. // our close tag..
  415. auto closeTag = input.indexOf ((juce_wchar) '>');
  416. if (closeTag >= 0)
  417. input += closeTag + 1;
  418. break;
  419. }
  420. if (c1 == '!' && CharacterFunctions::compareUpTo (input + 2, CharPointer_ASCII ("[CDATA["), 7) == 0)
  421. {
  422. input += 9;
  423. auto inputStart = input;
  424. for (;;)
  425. {
  426. auto c0 = *input;
  427. if (c0 == 0)
  428. {
  429. setLastError ("unterminated CDATA section", false);
  430. outOfData = true;
  431. break;
  432. }
  433. if (c0 == ']' && input[1] == ']' && input[2] == '>')
  434. {
  435. childAppender.append (XmlElement::createTextElement (String (inputStart, input)));
  436. input += 3;
  437. break;
  438. }
  439. ++input;
  440. }
  441. }
  442. else
  443. {
  444. // this is some other element, so parse and add it..
  445. if (auto* n = readNextElement (true))
  446. childAppender.append (n);
  447. else
  448. break;
  449. }
  450. }
  451. else // must be a character block
  452. {
  453. input = preWhitespaceInput; // roll back to include the leading whitespace
  454. MemoryOutputStream textElementContent;
  455. bool contentShouldBeUsed = ! ignoreEmptyTextElements;
  456. for (;;)
  457. {
  458. auto c = *input;
  459. if (c == '<')
  460. {
  461. if (input[1] == '!' && input[2] == '-' && input[3] == '-')
  462. {
  463. input += 4;
  464. auto closeComment = input.indexOf (CharPointer_ASCII ("-->"));
  465. if (closeComment < 0)
  466. {
  467. setLastError ("unterminated comment", false);
  468. outOfData = true;
  469. return;
  470. }
  471. input += closeComment + 3;
  472. continue;
  473. }
  474. break;
  475. }
  476. if (c == 0)
  477. {
  478. setLastError ("unmatched tags", false);
  479. outOfData = true;
  480. return;
  481. }
  482. if (c == '&')
  483. {
  484. String entity;
  485. readEntity (entity);
  486. if (entity.startsWithChar ('<') && entity [1] != 0)
  487. {
  488. auto oldInput = input;
  489. auto oldOutOfData = outOfData;
  490. input = entity.getCharPointer();
  491. outOfData = false;
  492. while (auto* n = readNextElement (true))
  493. childAppender.append (n);
  494. input = oldInput;
  495. outOfData = oldOutOfData;
  496. }
  497. else
  498. {
  499. textElementContent << entity;
  500. contentShouldBeUsed = contentShouldBeUsed || entity.containsNonWhitespaceChars();
  501. }
  502. }
  503. else
  504. {
  505. for (;; ++input)
  506. {
  507. auto nextChar = *input;
  508. if (nextChar == '\r')
  509. {
  510. nextChar = '\n';
  511. if (input[1] == '\n')
  512. continue;
  513. }
  514. if (nextChar == '<' || nextChar == '&')
  515. break;
  516. if (nextChar == 0)
  517. {
  518. setLastError ("unmatched tags", false);
  519. outOfData = true;
  520. return;
  521. }
  522. textElementContent.appendUTF8Char (nextChar);
  523. contentShouldBeUsed = contentShouldBeUsed || ! CharacterFunctions::isWhitespace (nextChar);
  524. }
  525. }
  526. }
  527. if (contentShouldBeUsed)
  528. childAppender.append (XmlElement::createTextElement (textElementContent.toUTF8()));
  529. }
  530. }
  531. }
  532. void XmlDocument::readEntity (String& result)
  533. {
  534. // skip over the ampersand
  535. ++input;
  536. if (input.compareIgnoreCaseUpTo (CharPointer_ASCII ("amp;"), 4) == 0)
  537. {
  538. input += 4;
  539. result += '&';
  540. }
  541. else if (input.compareIgnoreCaseUpTo (CharPointer_ASCII ("quot;"), 5) == 0)
  542. {
  543. input += 5;
  544. result += '"';
  545. }
  546. else if (input.compareIgnoreCaseUpTo (CharPointer_ASCII ("apos;"), 5) == 0)
  547. {
  548. input += 5;
  549. result += '\'';
  550. }
  551. else if (input.compareIgnoreCaseUpTo (CharPointer_ASCII ("lt;"), 3) == 0)
  552. {
  553. input += 3;
  554. result += '<';
  555. }
  556. else if (input.compareIgnoreCaseUpTo (CharPointer_ASCII ("gt;"), 3) == 0)
  557. {
  558. input += 3;
  559. result += '>';
  560. }
  561. else if (*input == '#')
  562. {
  563. int64_t charCode = 0;
  564. ++input;
  565. if (*input == 'x' || *input == 'X')
  566. {
  567. ++input;
  568. int numChars = 0;
  569. while (input[0] != ';')
  570. {
  571. auto hexValue = CharacterFunctions::getHexDigitValue (input[0]);
  572. if (hexValue < 0 || ++numChars > 8)
  573. {
  574. setLastError ("illegal escape sequence", true);
  575. break;
  576. }
  577. charCode = (charCode << 4) | hexValue;
  578. ++input;
  579. }
  580. ++input;
  581. }
  582. else if (input[0] >= '0' && input[0] <= '9')
  583. {
  584. int numChars = 0;
  585. for (;;)
  586. {
  587. const auto firstChar = input[0];
  588. if (firstChar == 0)
  589. {
  590. setLastError ("unexpected end of input", true);
  591. return;
  592. }
  593. if (firstChar == ';')
  594. break;
  595. if (++numChars > 12)
  596. {
  597. setLastError ("illegal escape sequence", true);
  598. break;
  599. }
  600. charCode = charCode * 10 + ((int) firstChar - '0');
  601. ++input;
  602. }
  603. ++input;
  604. }
  605. else
  606. {
  607. setLastError ("illegal escape sequence", true);
  608. result += '&';
  609. return;
  610. }
  611. result << (juce_wchar) charCode;
  612. }
  613. else
  614. {
  615. auto entityNameStart = input;
  616. auto closingSemiColon = input.indexOf ((juce_wchar) ';');
  617. if (closingSemiColon < 0)
  618. {
  619. outOfData = true;
  620. result += '&';
  621. }
  622. else
  623. {
  624. input += closingSemiColon + 1;
  625. result += expandExternalEntity (String (entityNameStart, (size_t) closingSemiColon));
  626. }
  627. }
  628. }
  629. String XmlDocument::expandEntity (const String& ent)
  630. {
  631. if (ent.equalsIgnoreCase ("amp")) return String::charToString ('&');
  632. if (ent.equalsIgnoreCase ("quot")) return String::charToString ('"');
  633. if (ent.equalsIgnoreCase ("apos")) return String::charToString ('\'');
  634. if (ent.equalsIgnoreCase ("lt")) return String::charToString ('<');
  635. if (ent.equalsIgnoreCase ("gt")) return String::charToString ('>');
  636. if (ent[0] == '#')
  637. {
  638. auto char1 = ent[1];
  639. if (char1 == 'x' || char1 == 'X')
  640. return String::charToString (static_cast<juce_wchar> (ent.substring (2).getHexValue32()));
  641. if (char1 >= '0' && char1 <= '9')
  642. return String::charToString (static_cast<juce_wchar> (ent.substring (1).getIntValue()));
  643. setLastError ("illegal escape sequence", false);
  644. return String::charToString ('&');
  645. }
  646. return expandExternalEntity (ent);
  647. }
  648. String XmlDocument::expandExternalEntity (const String& entity)
  649. {
  650. if (needToLoadDTD)
  651. {
  652. if (dtdText.isNotEmpty())
  653. {
  654. dtdText = dtdText.trimCharactersAtEnd (">");
  655. tokenisedDTD.addTokens (dtdText, true);
  656. if (tokenisedDTD[tokenisedDTD.size() - 2].equalsIgnoreCase ("system")
  657. && tokenisedDTD[tokenisedDTD.size() - 1].isQuotedString())
  658. {
  659. auto fn = tokenisedDTD[tokenisedDTD.size() - 1];
  660. tokenisedDTD.clear();
  661. tokenisedDTD.addTokens (getFileContents (fn), true);
  662. }
  663. else
  664. {
  665. tokenisedDTD.clear();
  666. auto openBracket = dtdText.indexOfChar ('[');
  667. if (openBracket > 0)
  668. {
  669. auto closeBracket = dtdText.lastIndexOfChar (']');
  670. if (closeBracket > openBracket)
  671. tokenisedDTD.addTokens (dtdText.substring (openBracket + 1,
  672. closeBracket), true);
  673. }
  674. }
  675. for (int i = tokenisedDTD.size(); --i >= 0;)
  676. {
  677. if (tokenisedDTD[i].startsWithChar ('%')
  678. && tokenisedDTD[i].endsWithChar (';'))
  679. {
  680. auto parsed = getParameterEntity (tokenisedDTD[i].substring (1, tokenisedDTD[i].length() - 1));
  681. StringArray newToks;
  682. newToks.addTokens (parsed, true);
  683. tokenisedDTD.remove (i);
  684. for (int j = newToks.size(); --j >= 0;)
  685. tokenisedDTD.insert (i, newToks[j]);
  686. }
  687. }
  688. }
  689. needToLoadDTD = false;
  690. }
  691. for (int i = 0; i < tokenisedDTD.size(); ++i)
  692. {
  693. if (tokenisedDTD[i] == entity)
  694. {
  695. if (tokenisedDTD[i - 1].equalsIgnoreCase ("<!entity"))
  696. {
  697. auto ent = tokenisedDTD [i + 1].trimCharactersAtEnd (">").trim().unquoted();
  698. // check for sub-entities..
  699. auto ampersand = ent.indexOfChar ('&');
  700. while (ampersand >= 0)
  701. {
  702. auto semiColon = ent.indexOf (i + 1, ";");
  703. if (semiColon < 0)
  704. {
  705. setLastError ("entity without terminating semi-colon", false);
  706. break;
  707. }
  708. auto resolved = expandEntity (ent.substring (i + 1, semiColon));
  709. ent = ent.substring (0, ampersand)
  710. + resolved
  711. + ent.substring (semiColon + 1);
  712. ampersand = ent.indexOfChar (semiColon + 1, '&');
  713. }
  714. return ent;
  715. }
  716. }
  717. }
  718. setLastError ("unknown entity", true);
  719. return entity;
  720. }
  721. String XmlDocument::getParameterEntity (const String& entity)
  722. {
  723. for (int i = 0; i < tokenisedDTD.size(); ++i)
  724. {
  725. if (tokenisedDTD[i] == entity
  726. && tokenisedDTD [i - 1] == "%"
  727. && tokenisedDTD [i - 2].equalsIgnoreCase ("<!entity"))
  728. {
  729. auto ent = tokenisedDTD [i + 1].trimCharactersAtEnd (">");
  730. if (ent.equalsIgnoreCase ("system"))
  731. return getFileContents (tokenisedDTD [i + 2].trimCharactersAtEnd (">"));
  732. return ent.trim().unquoted();
  733. }
  734. }
  735. return entity;
  736. }
  737. }