--- /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 <http://www.gnu.org/licenses/>. + */ + +#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<QString, decltype(LDHeader::type)> 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<modelobjects::Comment>(line); +}