551 lines
13 KiB
C++
551 lines
13 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include "ultima/shared/conf/xml_node.h"
|
|
#include "ultima/shared/conf/xml_tree.h"
|
|
#include "common/file.h"
|
|
|
|
namespace Ultima {
|
|
namespace Shared {
|
|
|
|
XMLNode::~XMLNode() {
|
|
for (auto *node : _nodeList) {
|
|
delete node;
|
|
}
|
|
}
|
|
|
|
const Common::String &XMLNode::reference(const Common::String &h, bool &exists) {
|
|
if (h.find('/') == Common::String::npos) {
|
|
// Must refer to me.
|
|
if (_id == h) {
|
|
exists = true;
|
|
return _text;
|
|
}
|
|
} else {
|
|
// Otherwise we want to split the Common::String at the first /
|
|
// then locate the branch to walk, and pass the rest
|
|
// down.
|
|
|
|
Common::String k;
|
|
k = h.substr(h.find('/') + 1);
|
|
Common::String k2 = k.substr(0, k.find('/'));
|
|
for (auto *node : _nodeList) {
|
|
if (node->_id == k2)
|
|
return node->reference(k, exists);
|
|
}
|
|
}
|
|
|
|
exists = false;
|
|
return _emptyString;
|
|
}
|
|
|
|
|
|
const XMLNode *XMLNode::subtree(const Common::String &h) const {
|
|
if (h.find('/') == Common::String::npos) {
|
|
// Must refer to me.
|
|
if (_id.equalsIgnoreCase(h))
|
|
return this;
|
|
} else {
|
|
// Otherwise we want to split the Common::String at the first /
|
|
// then locate the branch to walk, and pass the rest
|
|
// down.
|
|
|
|
Common::String k;
|
|
k = h.substr(h.find('/') + 1);
|
|
Common::String k2 = k.substr(0, k.find('/'));
|
|
for (auto *node : _nodeList) {
|
|
if (node->_id.equalsIgnoreCase(k2)) {
|
|
return node->subtree(k);
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
Common::String XMLNode::dump(int depth) {
|
|
Common::String s;
|
|
for (int i = 0; i < depth; ++i)
|
|
s += ' ';
|
|
|
|
s += "<";
|
|
s += _id;
|
|
s += ">";
|
|
if (_id[_id.size() - 1] != '/') {
|
|
if (_nodeList.empty() == false)
|
|
s += "\n";
|
|
for (auto *node : _nodeList) {
|
|
s += node->dump(depth + 1);
|
|
}
|
|
|
|
if (!_text.empty()) {
|
|
//s += Common::String(depth,' ');
|
|
s += encodeEntity(_text);
|
|
}
|
|
if (_id[0] == '?') {
|
|
return s;
|
|
}
|
|
//if(_content.size())
|
|
// s += "\n";
|
|
|
|
if (!_noClose) {
|
|
if (_text.empty()) {
|
|
for (int i = 0; i < depth; ++i)
|
|
s += ' ';
|
|
}
|
|
|
|
s += "</";
|
|
s += closeTag(_id);
|
|
s += ">\n";
|
|
}
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
void XMLNode::xmlAssign(const Common::String &key, const Common::String &value) {
|
|
if (key.find('/') == Common::String::npos) {
|
|
// Must refer to me.
|
|
if (_id == key)
|
|
_text = value;
|
|
else
|
|
error("Walking the XML tree failed to create a final node.");
|
|
return;
|
|
}
|
|
Common::String k;
|
|
k = key.substr(key.find('/') + 1);
|
|
Common::String k2 = k.substr(0, k.find('/'));
|
|
for (auto *node : _nodeList) {
|
|
if (node->_id == k2) {
|
|
node->xmlAssign(k, value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No match, so create a new node and do recursion
|
|
XMLNode *t = new XMLNode(_tree);
|
|
t->_parent = this;
|
|
t->_id = k2;
|
|
_nodeList.push_back(t);
|
|
(*t).xmlAssign(k, value);
|
|
}
|
|
|
|
|
|
void XMLNode::listKeys(const Common::String &key, Common::Array<Common::String> &vs,
|
|
bool longformat) const {
|
|
Common::String s(key);
|
|
s += "/";
|
|
|
|
for (auto *node : _nodeList) {
|
|
if (!longformat)
|
|
vs.push_back(node->_id);
|
|
else
|
|
vs.push_back(s + node->_id);
|
|
}
|
|
}
|
|
|
|
Common::String XMLNode::encodeEntity(const Common::String &s) {
|
|
Common::String ret;
|
|
|
|
for (const auto &c : s) {
|
|
switch (c) {
|
|
case '<':
|
|
ret += "<";
|
|
break;
|
|
case '>':
|
|
ret += ">";
|
|
break;
|
|
case '"':
|
|
ret += """;
|
|
break;
|
|
case '\'':
|
|
ret += "'";
|
|
break;
|
|
case '&':
|
|
ret += "&";
|
|
break;
|
|
default:
|
|
ret += c;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static Common::String decode_entity(const Common::String &s, size_t &pos) {
|
|
// size_t old_pos = pos;
|
|
size_t entityNameLen = s.findFirstOf("; \t\r\n", pos) - pos - 1;
|
|
|
|
/* Call me paranoid... but I don't think having an end-of-line or similar
|
|
inside a &...; expression is 'good', valid though it may be. */
|
|
assert(s[pos + entityNameLen + 1] == ';');
|
|
|
|
Common::String entity_name = s.substr(pos + 1, entityNameLen);
|
|
|
|
pos += entityNameLen + 2;
|
|
|
|
// Std::cout << "DECODE: " << entity_name << endl;
|
|
|
|
if (entity_name == "amp")
|
|
return Common::String("&");
|
|
else if (entity_name == "apos")
|
|
return Common::String("'");
|
|
else if (entity_name == "quot")
|
|
return Common::String("\"");
|
|
else if (entity_name == "lt")
|
|
return Common::String("<");
|
|
else if (entity_name == "gt")
|
|
return Common::String(">");
|
|
else if (entity_name.hasPrefix("#")) {
|
|
entity_name.deleteChar(0);
|
|
|
|
if (entity_name.hasPrefix("x")) {
|
|
uint tmp = 0;
|
|
int read = sscanf(entity_name.c_str() + 1, "%xh", &tmp);
|
|
if (read < 1)
|
|
error("strToInt failed on string \"%s\"", entity_name.c_str());
|
|
return Common::String((char)tmp);
|
|
} else {
|
|
uint tmp = atol(entity_name.c_str());
|
|
return Common::String((char)tmp);
|
|
}
|
|
} else {
|
|
error("Invalid xml encoded entity - %s", entity_name.c_str());
|
|
}
|
|
}
|
|
|
|
XMLNode *XMLNode::xmlParseDoc(XMLTree *tree, const Common::String &s) {
|
|
Common::String sbuf(s);
|
|
size_t nn = 0;
|
|
bool parsedXmlElement = false, parsedDocType = false;
|
|
XMLNode *node = nullptr, *child = nullptr;
|
|
|
|
for (;;) {
|
|
while (nn < s.size() && Common::isSpace(s[nn]))
|
|
++nn;
|
|
if (nn >= s.size())
|
|
return node;
|
|
|
|
if (s[nn] != '<') {
|
|
warning("expected '<' while reading config file, found %c\n", s[nn]);
|
|
return nullptr;
|
|
}
|
|
++nn;
|
|
|
|
if (nn < s.size() && s[nn] == '?') {
|
|
assert(!parsedXmlElement);
|
|
parsedXmlElement = true;
|
|
nn = s.findFirstOf('>', nn);
|
|
} else if (nn < s.size() && s.substr(nn, 8).equalsIgnoreCase("!doctype")) {
|
|
assert(!parsedDocType);
|
|
parsedDocType = true;
|
|
parseDocTypeElement(s, nn);
|
|
} else {
|
|
--nn;
|
|
child = xmlParse(tree, sbuf, nn);
|
|
if (child) {
|
|
if (node)
|
|
error("Invalid multiple xml nodes at same level");
|
|
node = child;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// If this point was reached, we just skipped ?xml or doctype element
|
|
++nn;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
void XMLNode::parseDocTypeElement(const Common::String &s, size_t &nn) {
|
|
nn = s.findFirstOf(">[", nn);
|
|
if (nn == Common::String::npos)
|
|
// No ending tag
|
|
return;
|
|
|
|
if (s[nn] == '[') {
|
|
// Square bracketed area
|
|
nn = s.findFirstOf(']', nn) + 1;
|
|
}
|
|
|
|
if (nn >= s.size() || s[nn] != '>')
|
|
nn = Common::String::npos;
|
|
}
|
|
|
|
XMLNode *XMLNode::xmlParse(XMLTree *tree, const Common::String &s, size_t &pos) {
|
|
bool inTag = false, isSelfEnding = false;
|
|
XMLNode *node = nullptr, *child = nullptr;
|
|
Common::String nodeText, plainText;
|
|
|
|
while (pos < s.size()) {
|
|
if (inTag && nodeText.hasPrefix("!--") && (s[pos] != '>' || !nodeText.hasSuffix("--"))) {
|
|
// It's a > within the comment, but not the terminator
|
|
nodeText += s[pos];
|
|
++pos;
|
|
continue;
|
|
}
|
|
|
|
switch (s[pos]) {
|
|
case '<':
|
|
// Start of tag
|
|
assert(!inTag);
|
|
|
|
trim(plainText);
|
|
if (!plainText.empty()) {
|
|
// Plain text, return it
|
|
child = new XMLNode(tree);
|
|
child->_text = plainText;
|
|
return child;
|
|
}
|
|
|
|
// New tag?
|
|
if (s[pos + 1] == '/') {
|
|
// No. It's a close tag. Move beyond it
|
|
while (s[pos] != '>')
|
|
pos++;
|
|
++pos;
|
|
// Return nullptr as indication that the end was reached
|
|
return nullptr;
|
|
}
|
|
|
|
inTag = true;
|
|
++pos;
|
|
break;
|
|
|
|
case '>':
|
|
// End of tag
|
|
isSelfEnding = nodeText.hasSuffix("/");
|
|
++pos;
|
|
|
|
if (isSelfEnding) {
|
|
nodeText.deleteLastChar();
|
|
} else if (nodeText.hasPrefix("!--")) {
|
|
// Comment element that can be ignored
|
|
inTag = false;
|
|
nodeText.clear();
|
|
plainText.clear();
|
|
break;
|
|
}
|
|
|
|
// Create a node for the tag, and parse it's attributes, if any
|
|
node = new XMLNode(tree);
|
|
node->parseNodeText(nodeText);
|
|
|
|
if (!isSelfEnding) {
|
|
// Iterate through parsing and adding sub-elements
|
|
while ((child = xmlParse(tree, s, pos)) != nullptr) {
|
|
child->_parent = node;
|
|
node->_nodeList.push_back(child);
|
|
}
|
|
} else if (node->_id.equalsIgnoreCase("xi:include")) {
|
|
// Element is a placeholder for inclusion of a secondary XML file
|
|
Common::String fname = node->_attributes["href"];
|
|
delete node;
|
|
node = xmlParseFile(tree, Common::Path(fname, '/'));
|
|
}
|
|
|
|
return node;
|
|
|
|
case '&':
|
|
if (inTag)
|
|
nodeText += decode_entity(s, pos);
|
|
else
|
|
plainText += decode_entity(s, pos);
|
|
break;
|
|
default:
|
|
if (inTag)
|
|
nodeText += s[pos++];
|
|
else
|
|
plainText += s[pos++];
|
|
break;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
void XMLNode::parseNodeText(const Common::String &nodeText) {
|
|
size_t firstSpace = nodeText.findFirstOf(' ');
|
|
if (firstSpace == Common::String::npos) {
|
|
// The entire text is the id
|
|
_id = nodeText;
|
|
return;
|
|
}
|
|
|
|
// Set the Id and get out the remaining attributes section, if any
|
|
_id = Common::String(nodeText.c_str(), firstSpace);
|
|
Common::String attr(nodeText.c_str() + firstSpace);
|
|
|
|
for (;;) {
|
|
// Skip any spaces
|
|
while (!attr.empty() && Common::isSpace(attr[0]))
|
|
attr.deleteChar(0);
|
|
if (attr.empty())
|
|
return;
|
|
|
|
// Find the equals after the attribute name
|
|
size_t equalsPos = attr.findFirstOf('=');
|
|
if (equalsPos == Common::String::npos)
|
|
return;
|
|
|
|
// Get the name, and find the quotes start
|
|
Common::String name = Common::String(attr.c_str(), equalsPos);
|
|
++equalsPos;
|
|
while (equalsPos < attr.size() && Common::isSpace(attr[equalsPos]))
|
|
++equalsPos;
|
|
|
|
if (attr[equalsPos] == '\'' && attr[equalsPos] != '"')
|
|
return;
|
|
|
|
// Find the end of the attribute
|
|
size_t attrEnd = attr.findFirstOf(attr[equalsPos], equalsPos + 1);
|
|
if (attrEnd == Common::String::npos)
|
|
return;
|
|
|
|
// Add the parsed attribute
|
|
_attributes[name] = Common::String(attr.c_str() + equalsPos + 1, attr.c_str() + attrEnd);
|
|
|
|
// Remove the parsed attribute
|
|
attr = Common::String(attr.c_str() + attrEnd + 1);
|
|
}
|
|
}
|
|
|
|
XMLNode *XMLNode::xmlParseFile(XMLTree *tree, const Common::Path &fname) {
|
|
const Common::Path rootFile = tree->_filename;
|
|
Common::Path filename = rootFile.getParent().join(fname);
|
|
|
|
Common::File f;
|
|
if (!f.open(filename))
|
|
error("Could not open xml file - %s", filename.toString().c_str());
|
|
|
|
// Read in the file contents
|
|
char *buf = new char[f.size() + 1];
|
|
f.read(buf, f.size());
|
|
buf[f.size()] = '\0';
|
|
Common::String text(buf, buf + f.size());
|
|
delete[] buf;
|
|
f.close();
|
|
|
|
// Parse the sub-xml
|
|
XMLNode *result = xmlParseDoc(tree, text);
|
|
if (!result)
|
|
error("Error passing xml - %s", fname.toString().c_str());
|
|
|
|
return result;
|
|
}
|
|
|
|
bool XMLNode::searchPairs(KeyTypeList &ktl, const Common::String &basekey,
|
|
const Common::String &currkey, const unsigned int pos) {
|
|
/* If our 'current key' is longer then the key we're serching for
|
|
we've obviously gone too deep in this branch, and we won't find
|
|
it here. */
|
|
if ((currkey.size() <= basekey.size()) && (_id[0] != '!')) {
|
|
/* If we've found it, return every key->value pair under this key,
|
|
then return true, since we've found the key we were looking for.*/
|
|
if (basekey == currkey + _id) {
|
|
for (auto *node : _nodeList) {
|
|
if (node->_id[0] != '!')
|
|
node->selectPairs(ktl, "");
|
|
}
|
|
return true;
|
|
}
|
|
/* Else, keep searching for the key under it's subnodes */
|
|
else {
|
|
for (auto *node : _nodeList) {
|
|
if (node->searchPairs(ktl, basekey, currkey + _id + '/', pos))
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Just adds every key->value pair under the this node to the ktl */
|
|
void XMLNode::selectPairs(KeyTypeList &ktl, const Common::String &currkey) {
|
|
ktl.push_back(KeyType(currkey + _id, currkey));
|
|
|
|
for (auto *node : _nodeList) {
|
|
node->selectPairs(ktl, currkey + _id + '/');
|
|
}
|
|
}
|
|
|
|
XMLNode *XMLNode::getPrior() const {
|
|
const Common::Array<XMLNode *> &siblings = _parent->_nodeList;
|
|
for (uint idx = 0; idx < siblings.size(); ++idx) {
|
|
if (siblings[idx] == this)
|
|
return (idx > 0) ? siblings[idx - 1] : nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
XMLNode *XMLNode::getNext() const {
|
|
const Common::Array<XMLNode *> &siblings = _parent->_nodeList;
|
|
for (uint idx = 0; idx < siblings.size(); ++idx) {
|
|
if (siblings[idx] == this)
|
|
return (idx < (siblings.size() - 1)) ? siblings[idx + 1] : nullptr;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void XMLNode::freeDoc() {
|
|
delete _tree;
|
|
}
|
|
|
|
void XMLNode::trim(Common::String &s) {
|
|
// Convert any CRLF to just LF
|
|
size_t pos;
|
|
while ((pos = s.find("\r\n")) != Common::String::npos)
|
|
s.deleteChar(pos);
|
|
|
|
// Then check if the string is entirely whitespace
|
|
bool hasContent = false;
|
|
for (uint idx = 0; idx < s.size() && !hasContent; ++idx)
|
|
hasContent = !Common::isSpace(s[idx]);
|
|
|
|
if (!hasContent) {
|
|
s = "";
|
|
return;
|
|
}
|
|
|
|
// Remove any spaces from the very start of the string and following
|
|
// any linefeeds. This handles any indented text within the xml
|
|
for (size_t startPos = 0; startPos != Common::String::npos;
|
|
startPos = s.findFirstOf('\n', startPos + 1)) {
|
|
pos = (startPos == 0) ? 0 : startPos + 1;
|
|
while (pos < s.size() && s[pos] == ' ')
|
|
s.deleteChar(pos);
|
|
}
|
|
}
|
|
|
|
Common::String XMLNode::closeTag(const Common::String &s) {
|
|
if (s.find(" ") == Common::String::npos)
|
|
return s;
|
|
|
|
return s.substr(0, s.find(" "));
|
|
}
|
|
|
|
} // End of namespace Shared
|
|
} // End of namespace Ultima
|