diff -r 2bdc3ac5e77c -r 55a55a9ec2c2 src/parser.cpp
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/parser.cpp Sun Sep 22 11:51:41 2019 +0300
@@ -0,0 +1,270 @@
+/*
+ * LDForge: LDraw parts authoring CAD
+ * Copyright (C) 2013 - 2018 Teemu Piippo
+ *
+ * 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 .
+ */
+
+#include "model.h"
+#include "parser.h"
+#include "objecttypes/comment.h"
+#include "objecttypes/conditionaledge.h"
+#include "objecttypes/edge.h"
+#include "objecttypes/errorline.h"
+#include "objecttypes/modelobject.h"
+#include "objecttypes/polygon.h"
+#include "objecttypes/subfilereference.h"
+
+/*
+ * Constructs an LDraw parser
+ */
+Parser::Parser(QIODevice& device, QObject* parent) :
+ QObject {parent},
+ device {device} {}
+
+/*
+ * Reads a single line from the device.
+ */
+QString Parser::readLine()
+{
+ return QString::fromUtf8(this->device.readLine()).trimmed();
+}
+
+const QMap Parser::typeStrings {
+ {"Part", LDHeader::Part},
+ {"Subpart", LDHeader::Subpart},
+ {"Shortcut", LDHeader::Shortcut},
+ {"Primitive", LDHeader::Primitive},
+ {"8_Primitive", LDHeader::Primitive_8},
+ {"48_Primitive", LDHeader::Primitive_48},
+ {"Configuration", LDHeader::Configuration},
+};
+
+/*
+ * Parses a single line of the header.
+ * Possible parse results:
+ * · ParseSuccess: the header line was parsed successfully.
+ * · ParseFailure: the header line was parsed incorrectly and needs to be handled otherwise.
+ * · StopParsing: the line does not belong in the header and header parsing needs to stop.
+ */
+Parser::HeaderParseResult Parser::parseHeaderLine(
+ LDHeader& header,
+ Winding& winding,
+ const QString& line
+) {
+ if (line.isEmpty())
+ {
+ return ParseSuccess;
+ }
+ else if (not line.startsWith("0") or line.startsWith("0 //"))
+ {
+ return StopParsing;
+ }
+ else if (line.startsWith("0 !LDRAW_ORG "))
+ {
+ QStringList tokens = line
+ .mid(strlen("0 !LDRAW_ORG "))
+ .split(" ", QString::SkipEmptyParts);
+
+ if (not tokens.isEmpty())
+ {
+ QString partTypeString = tokens[0];
+ // Anything that enters LDForge becomes unofficial in any case if saved.
+ // Therefore we don't need to give the Unofficial type any special
+ // consideration.
+ if (partTypeString.startsWith("Unofficial_"))
+ partTypeString = partTypeString.mid(strlen("Unofficial_"));
+ header.type = Parser::typeStrings.value(partTypeString, LDHeader::Part);
+ header.qualfiers = 0;
+ if (tokens.contains("Alias"))
+ header.qualfiers |= LDHeader::Alias;
+ if (tokens.contains("Physical_Color"))
+ header.qualfiers |= LDHeader::Physical_Color;
+ if (tokens.contains("Flexible_Section"))
+ header.qualfiers |= LDHeader::Flexible_Section;
+ return ParseSuccess;
+ }
+ else
+ {
+ return ParseFailure;
+ }
+ }
+ else if (line == "0 BFC CERTIFY CCW")
+ {
+ winding = CounterClockwise;
+ return ParseSuccess;
+ }
+ else if (line == "0 BFC CERTIFY CW")
+ {
+ winding = Clockwise;
+ return ParseSuccess;
+ }
+ else if (line == "0 BFC NOCERTIFY")
+ {
+ winding = NoWinding;
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !HISTORY "))
+ {
+ static const QRegExp historyRegexp {
+ R"(0 !HISTORY\s+(\d{4}-\d{2}-\d{2})\s+)"
+ R"((\{[^}]+|\[[^]]+)[\]}]\s+(.+))"
+ };
+ if (historyRegexp.exactMatch(line))
+ {
+ QString dateString = historyRegexp.capturedTexts().value(1);
+ QString authorWithPrefix = historyRegexp.capturedTexts().value(2);
+ QString description = historyRegexp.capturedTexts().value(3);
+ LDHeader::HistoryEntry historyEntry;
+ historyEntry.date = QDate::fromString(dateString, Qt::ISODate);
+ historyEntry.description = description;
+
+ if (authorWithPrefix[0] == '{')
+ historyEntry.author = authorWithPrefix + "}";
+ else
+ historyEntry.author = authorWithPrefix.mid(1);
+
+ header.history.append(historyEntry);
+ return ParseSuccess;
+ }
+ else
+ {
+ return ParseFailure;
+ }
+ }
+ else if (line.startsWith("0 Author: "))
+ {
+ header.author = line.mid(strlen("0 Author: "));
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 Name: "))
+ {
+ header.name = line.mid(strlen("0 Name: "));
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !HELP "))
+ {
+ if (not header.help.isEmpty())
+ header.help += "\n";
+ header.help += line.mid(strlen("0 !HELP "));
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !KEYWORDS "))
+ {
+ if (not header.keywords.isEmpty())
+ header.keywords += "\n";
+ header.keywords += line.mid(strlen("0 !KEYWORDS "));
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !CATEGORY "))
+ {
+ header.category = line.mid(strlen("0 !CATEGORY "));
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !CMDLINE "))
+ {
+ header.cmdline = line.mid(strlen("0 !CMDLINE "));
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !LICENSE Redistributable under CCAL version 2.0"))
+ {
+ header.license = LDHeader::CaLicense;
+ return ParseSuccess;
+ }
+ else if (line.startsWith("0 !LICENSE Not redistributable"))
+ {
+ header.license = LDHeader::NonCaLicense;
+ return ParseSuccess;
+ }
+ else
+ {
+ return ParseFailure;
+ }
+}
+
+/*
+ * Parses the header from the device given at construction and returns
+ * the resulting header structure.
+ */
+LDHeader Parser::parseHeader(Winding& winding)
+{
+ LDHeader header = {};
+
+ if (not this->device.atEnd())
+ {
+ // Parse the description
+ QString descriptionLine = this->readLine();
+ if (descriptionLine.startsWith("0 "))
+ {
+ header.description = descriptionLine.mid(strlen("0 ")).trimmed();
+
+ // Parse the rest of the header
+ while (not this->device.atEnd())
+ {
+ const QString& line = this->readLine();
+ auto result = parseHeaderLine(header, winding, line);
+
+ if (result == ParseFailure)
+ {
+ // Failed to parse this header line, add it as a comment into the body later.
+ this->bag.append(line);
+ }
+ else if (result == StopParsing)
+ {
+ // Header parsing stops, add this line to the body.
+ this->bag.append(line);
+ break;
+ }
+ }
+ }
+ else
+ {
+ this->bag.append(descriptionLine);
+ }
+ }
+
+ return header;
+}
+
+/**
+ * @brief Parses the model body into the given model.
+ * @param editor Handle to model edit context
+ */
+void Parser::parseBody(Model::EditContext& editor)
+{
+ bool invertNext = false;
+ while (not this->device.atEnd())
+ this->bag.append(this->readLine());
+ for (const QString& line : this->bag)
+ {
+ if (line == "0 BFC INVERTNEXT" or line == "0 BFC CERTIFY INVERTNEXT")
+ {
+ invertNext = true;
+ continue;
+ }
+ modelobjects::BaseObject* object = parseFromString(editor, line);
+ if (invertNext)
+ {
+ editor.setObjectProperty(object, modelobjects::Property::IsInverted, true);
+ }
+ invertNext = false;
+ }
+}
+
+modelobjects::BaseObject* Parser::parseFromString(
+ Model::EditContext& editor,
+ const QString& line)
+{
+ return editor.append(line);
+}