diff --git a/doomsday/libdeng2/data.pri b/doomsday/libdeng2/data.pri index ad44f22dfd..a874cc969a 100644 --- a/doomsday/libdeng2/data.pri +++ b/doomsday/libdeng2/data.pri @@ -8,6 +8,7 @@ HEADERS += \ include/de/Guard \ include/de/IBlock \ include/de/IByteArray \ + include/de/Info \ include/de/IReadable \ include/de/ISerializable \ include/de/IWritable \ @@ -26,6 +27,7 @@ HEADERS += \ include/de/data/huffman.h \ include/de/data/iblock.h \ include/de/data/ibytearray.h \ + include/de/data/info.h \ include/de/data/ireadable.h \ include/de/data/iserializable.h \ include/de/data/iwritable.h \ @@ -42,10 +44,11 @@ SOURCES += \ src/data/bytesubarray.cpp \ src/data/date.cpp \ src/data/fixedbytearray.cpp \ - src/data/huffman.cpp \ src/data/guard.cpp \ + src/data/huffman.cpp \ + src/data/info.cpp \ src/data/lockable.cpp \ - src/data/string.cpp \ src/data/reader.cpp \ + src/data/string.cpp \ src/data/time.cpp \ src/data/writer.cpp diff --git a/doomsday/libdeng2/include/de/Info b/doomsday/libdeng2/include/de/Info new file mode 100644 index 0000000000..16af1f265d --- /dev/null +++ b/doomsday/libdeng2/include/de/Info @@ -0,0 +1 @@ +#include "data/info.h" diff --git a/doomsday/libdeng2/include/de/data/info.h b/doomsday/libdeng2/include/de/data/info.h new file mode 100644 index 0000000000..0fce313a10 --- /dev/null +++ b/doomsday/libdeng2/include/de/data/info.h @@ -0,0 +1,194 @@ +/* + * The Doomsday Engine Project -- libdeng2 + * + * Copyright (c) 2012 Jaakko Keränen + * + * 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 2 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 . + */ + +#ifndef LIBDENG2_INFO_H +#define LIBDENG2_INFO_H + +#include "../String" +#include +#include + +namespace de { + +/** + * Key/value tree. Read from the "Snowberry" Info file format. + * + * This implementation has been ported to C++ based on cfparser.py from + * Snowberry. + * + * @todo Document Info syntax in wiki, with example. + */ +class Info +{ +public: + /** + * Base class for all elements. + */ + class Element { + public: + enum Type { + None, + Key, + List, + Block + }; + + /** + * @param name Case-independent name of the element. + */ + Element(Type t = None, const String& n = "") : _type(t) { setName(n); } + virtual ~Element() {} + + Type type() const { return _type; } + bool isKey() const { return _type == Key; } + bool isList() const { return _type == List; } + bool isBlock() const { return _type == Block; } + const String& name() const { return _name; } + + void setName(const String& name) { _name = name.toLower(); } + + virtual QStringList values() const = 0; + + private: + Type _type; + String _name; + }; + + /** + * Element that contains a single string value. + */ + class KeyElement : public Element { + public: + KeyElement(const String& name, const String& value) : Element(Key, name), _value(value) {} + + void setValue(const String& v) { _value = v; } + const String& value() const { return _value; } + + QStringList values() const { + QStringList list; + list << _value; + return list; + } + + private: + String _value; + }; + + /** + * Element that contains a list of string values. + */ + class ListElement : public Element { + public: + ListElement(const String& name) : Element(List, name) {} + + void add(const String& v) { _values << v; } + + QStringList values() const { return _values; } + + private: + QStringList _values; + }; + + /** + * Contains other Elements, including other block elements. In addition to + * a name, each block may have a "block type", which is a case insensitive + * identifier. + */ + class BlockElement : public Element { + public: + DENG2_ERROR(ValuesError) + + typedef QHash Contents; + typedef QList ContentsInOrder; + + BlockElement(const String& bType, const String& name) : Element(Block, name) { + setBlockType(bType); + } + ~BlockElement(); + + const String& blockType() const { return _blockType; } + const ContentsInOrder& contentsInOrder() const { return _contentsInOrder; } + const Contents& contents() const { return _contents; } + + QStringList values() const { + throw ValuesError("Info::BlockElement::values", + "Block elements do not contain text values (only other elements)"); + } + + int size() const { return _contents.size(); } + bool contains(const String& name) { return _contents.contains(name); } + + void setBlockType(const String& bType) { _blockType = bType.toLower(); } + void clear(); + void add(Element* elem) { + DENG2_ASSERT(elem != 0); + _contentsInOrder.append(elem); + _contents.insert(elem->name(), elem); + } + + Element* find(const String& name) const { + Contents::const_iterator found = _contents.find(name); + if(found == _contents.end()) return 0; + return found.value(); + } + + /** + * Finds the value of a key inside the block. If the element is not a + * key element, returns an empty string. + * + * @param name Name of a key element in the block. + */ + String keyValue(const String& name) const { + Element* e = find(name); + if(!e || !e->isKey()) return ""; + return static_cast(e)->value(); + } + + private: + String _blockType; + Contents _contents; + ContentsInOrder _contentsInOrder; + }; + +public: + /// The parser encountered a syntax error in the source file. @ingroup errors + DENG2_ERROR(SyntaxError) + +public: + Info(); + + /** + * Deserializes the key/value data from text. + * + * @param infoSource Info text. + */ + Info(const String& infoSource); + + ~Info(); + + const BlockElement& root() const; + +private: + struct Instance; + Instance* d; +}; + +} // namespace de + +#endif // LIBDENG2_INFO_H diff --git a/doomsday/libdeng2/src/data/info.cpp b/doomsday/libdeng2/src/data/info.cpp new file mode 100644 index 0000000000..5deda1cbac --- /dev/null +++ b/doomsday/libdeng2/src/data/info.cpp @@ -0,0 +1,467 @@ +/* + * The Doomsday Engine Project -- libdeng2 + * + * Copyright (c) 2012 Jaakko Keränen + * + * 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 2 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 . + */ + +#include "de/Info" + +using namespace de; + +const static QString WHITESPACE = " \t\r\n"; +const static QString WHITESPACE_OR_COMMENT = " \t\r\n#"; +const static QString TOKEN_BREAKING_CHARS = "#:=(){}<>,\"" + WHITESPACE; + +struct Info::Instance +{ + DENG2_ERROR(OutOfElements) + DENG2_ERROR(EndOfFile) + + Instance() : currentLine(0), cursor(0), tokenStartOffset(0), rootBlock("", "") + {} + + /** + * Initialize the parser for reading a block of source content. + * @param content Text to be parsed. + */ + void init(const String& source) + { + rootBlock.clear(); + + // The source data. Add an extra newline so the character reader won't + // get confused. + content = source + "\n"; + currentLine = 1; + + nextChar(); + tokenStartOffset = 0; + + // When nextToken() is called and the current token is empty, + // it is deduced that the source file has ended. We must + // therefore set a dummy token that will be discarded + // immediately. + currentToken = " "; + nextToken(); + } + + /** + * Returns the next character from the source file. + */ + QChar peekChar() + { + return currentChar; + } + + /** + * Move to the next character in the source file. + */ + void nextChar() + { + if(cursor >= content.size()) + { + // No more characters to read. + throw EndOfFile(QString("EOF on line %i").arg(currentLine)); + } + if(currentChar == '\n') currentLine++; + currentChar = content[cursor]; + cursor++; + } + + /** + * Read a line of text from the content and return it. + */ + String readLine() + { + String line; + nextChar(); + while(currentChar != '\n') + { + line += currentChar; + nextChar(); + } + return line; + } + + /** + * Read until a newline is encountered. Returns the contents of the line. + */ + String readToEOL() + { + cursor = tokenStartOffset; + String line = readLine(); + try + { + nextChar(); + } + catch(const EndOfFile&) + { + // If the file ends right after the line, we'll get the EOF + // exception. We can safely ignore it for now. + } + return line; + } + + String peekToken() + { + return currentToken; + } + + /** + * Returns the next meaningful token from the source file. + */ + String nextToken() + { + // Already drawn a blank? + if(currentToken.isEmpty()) throw EndOfFile("out of tokens"); + + currentToken = ""; + + try + { + // Skip over any whitespace. + while(WHITESPACE_OR_COMMENT.contains(peekChar())) + { + // Comments are considered whitespace. + if(peekChar() == '#') readLine(); + nextChar(); + } + + // Store the offset where the token begins. + tokenStartOffset = cursor; + + // The first nonwhite is accepted. + currentToken += peekChar(); + nextChar(); + + // Token breakers are tokens all by themselves. + if(TOKEN_BREAKING_CHARS.contains(currentToken[0])) + return currentToken; + + while(!TOKEN_BREAKING_CHARS.contains(peekChar())) + { + currentToken += peekChar(); + nextChar(); + } + } + catch(const EndOfFile&) + {} + + return currentToken; + } + + /** + * This is the method that the user calls to retrieve the next element from + * the source file. If there are no more elements to return, a + * OutOfElements exception is thrown. + * + * @return Parsed element. Caller gets owernship. + */ + Element* get() + { + Element* e = parseElement(); + if(!e) throw OutOfElements(""); + return e; + } + + /** + * Returns the next element from the source file. + * @return An instance of one of the Element classes, or @c NULL if there are none. + */ + Element* parseElement() + { + String key; + String next; + try + { + key = peekToken(); + + // The next token decides what kind of element we have here. + next = nextToken(); + } + catch(const EndOfFile&) + { + // The file ended. + return 0; + } + + if(next == ":" || next == "=") + { + return parseKeyElement(key); + } + else if(next == "<") + { + return parseListElement(key); + } + else + { + // It must be a block element. + return parseBlockElement(key); + } + } + + /** + * Parse a string literal. Returns the string sans the quotation marks in + * the beginning and the end. + */ + String parseString() + { + if(peekToken() != "\"") + { + throw SyntaxError("Info::parseString", + QString("Expected string to begin with '\"', but '%1' found instead (on line %2).") + .arg(peekToken()).arg(currentLine)); + } + + // The collected characters. + String chars; + + while(peekChar() != '"') + { + if(peekChar() == '\'') + { + // Double single quotes form a double quote ('' => "). + nextChar(); + if(peekChar() == '\'') + chars.append("\""); + else + { + chars.append("'"); + chars.append(peekChar()); + } + } + else + { + // Other characters are appended as-is, even newlines. + chars.append(peekChar()); + } + nextChar(); + } + + // Move the parser to the next token. + nextChar(); + nextToken(); + return chars; + } + + /** + * Parse a value from the source file. The current token + * should be on the first token of the value. Values come in + * different flavours: + * - single token + * - string literal (can be split) + */ + String parseValue() + { + String value; + + // Check if it is the beginning of a string literal. + if(peekToken() == "\"") + { + try + { + // The value will be composed of any number of sub-strings. + forever { value += parseString(); } + } + catch(const de::Error&) + { + // No more strings to append. + return value; + } + } + + // Then it must be a single token. + value = peekToken(); + nextToken(); + return value; + } + + /** + * Parse a key element. + * @param name Name of the parsed key element. + */ + KeyElement* parseKeyElement(const String& name) + { + String value; + + // A colon means that that the rest of the line is the value of + // the key element. + if(peekToken() == ":") + { + value = readToEOL().trimmed(); + nextToken(); + } + else + { + /** + * Key = + * "This is a long string " + * "that spans multiple lines." + */ + nextToken(); + value = parseValue(); + } + return new KeyElement(name, value); + } + + /** + * Parse a list element, identified by the given name. + */ + ListElement* parseListElement(const String& name) + { + if(peekToken() != "<") + { + throw SyntaxError("Info::parseListElement", + QString("List must begin with a '<', but '%1' found instead (on line %2).") + .arg(peekToken()).arg(currentLine)); + } + + QScopedPointer element(new ListElement(name)); + + /// List syntax: + /// list ::= list-identifier '<' [value {',' value}] '>' + /// list-identifier ::= token + + // Move past the opening angle bracket. + nextToken(); + + forever + { + element->add(parseValue()); + + // List elements are separated explicitly. + String separator = peekToken(); + nextToken(); + + // The closing bracket? + if(separator == ">") break; + + // There should be a comma here. + if(separator != ",") + { + throw SyntaxError("Info::parseListElement", + QString("List values must be separated with a comma, but '%1' found instead (on line %2).") + .arg(separator).arg(currentLine)); + } + } + return element.take(); + } + + /** + * Parse a block element, identified by the given name. + * @param blockType Identifier of the block. + */ + BlockElement* parseBlockElement(const String& blockType) + { + QScopedPointer block(new BlockElement(blockType, peekToken())); + int startLine = currentLine; + + // How about some attributes? + // Syntax: {token value} '('|'{' + + try + { + String endToken; + + nextToken(); + while(peekToken() != "(" && peekToken() != "{") + { + String keyName = peekToken(); + nextToken(); + String value = parseValue(); + + // This becomes a key element inside the block. + block->add(new KeyElement(keyName, value)); + } + + endToken = (peekToken() == "("? ")" : "}"); + + // Move past the opening parentheses. + nextToken(); + + while(peekToken() != endToken) + { + Element* element = parseElement(); + if(!element) + { + throw SyntaxError("Info::parseBlockElement", + QString("Block element was never closed, end of file encountered before '%1' was found (on line %2).") + .arg(endToken).arg(currentLine)); + } + block->add(element); + } + } + catch(const EndOfFile&) + { + throw SyntaxError("Info::parseBlockElement", + QString("End of file encountered unexpectedly while parsing a block element (block started on line %1).") + .arg(startLine)); + } + + // Move past the closing parentheses. + nextToken(); + + return block.take(); + } + + void parse(const String& source) + { + init(source); + forever + { + Element* e = parseElement(); + if(!e) break; + rootBlock.add(e); + } + } + + String content; + int currentLine; + int cursor; ///< Index of the next character from the source. + QChar currentChar; + int tokenStartOffset; + String currentToken; + BlockElement rootBlock; +}; + +Info::BlockElement::~BlockElement() +{ + clear(); +} + +void Info::BlockElement::clear() +{ + for(Contents::iterator i = _contents.begin(); i != _contents.end(); ++i) + delete i.value(); + + _contents.clear(); + _contentsInOrder.clear(); +} + +Info::Info(const String& infoSource) +{ + d = new Instance; + d->parse(infoSource); +} + +Info::~Info() +{ + delete d; +} + +const Info::BlockElement &Info::root() const +{ + return d->rootBlock; +}