src/parser.cpp

Thu, 03 Oct 2019 23:44:28 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Thu, 03 Oct 2019 23:44:28 +0300
changeset 6
73e448b2943d
parent 5
593a658cba8e
child 8
44679e468ba9
permissions
-rw-r--r--

language support

/*
 *  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();
}

static const QMap<QString, decltype(LDHeader::type)> 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 = 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;
	}
}

namespace
{
	namespace regexes
	{
		static const QRegExp comment {R"(^\s*0\s*\/\/\s*(.+)$)"};
		static const QRegExp metacommand {R"(^\s*0\s*(.+)$)"};
		static const QRegExp edgeline
		{
			R"(^\s*2)" // starting 2-token
			R"(\s+(\d+))" // colour
			R"(((?:\s+[^\s]+){3}))" // 1st vertex
			R"(((?:\s+[^\s]+){3}))" // 2nd vertex
			R"(\s*$)" // end
		};
		static const QRegExp triangle
		{
			R"(^\s*3)" // starting 3-token
			R"(\s+(\d+))" // colour
			R"(((?:\s+[^\s]+){3}))" // 1st vertex
			R"(((?:\s+[^\s]+){3}))" // 2nd vertex
			R"(((?:\s+[^\s]+){3}))" // 3rd vertex
			R"(\s*$)" // end
		};
		static const QRegExp quadrilateral
		{
			R"(^\s*4)" // starting 4-token
			R"(\s+(\d+))" // colour
			R"(((?:\s+[^\s]+){3}))" // 1st vertex
			R"(((?:\s+[^\s]+){3}))" // 2nd vertex
			R"(((?:\s+[^\s]+){3}))" // 3rd vertex
			R"(((?:\s+[^\s]+){3}))" // 4th vertex
			R"(\s*$)" // end
		};
		static const QRegExp conditionaledge
		{
			R"(^\s*5)" // starting 5-token
			R"(\s+(\d+))" // colour
			R"(((?:\s+[^\s]+){3}))" // 1st vertex
			R"(((?:\s+[^\s]+){3}))" // 2nd vertex
			R"(((?:\s+[^\s]+){3}))" // 1st control point
			R"(((?:\s+[^\s]+){3}))" // 2nd control point
			R"(\s*$)" // end
		};
	}
}

static Vertex vertexFromString(const QString& vertex_string)
{
	static const QRegExp pattern {R"(^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$)"};
	const bool succeeded = pattern.exactMatch(vertex_string);
	if (succeeded)
	{
		const float x = pattern.cap(1).toFloat(nullptr);
		const float y = pattern.cap(2).toFloat(nullptr);
		const float z = pattern.cap(3).toFloat(nullptr);
		return {x, y, z};
	}
	else
	{
		return {};
	}
}

static modelobjects::Edge* parseEdgeline(
	Model::EditContext& editor,
	const QString& line)
{
	const bool succeeded = regexes::edgeline.exactMatch(line);
	if (succeeded)
	{
		const Color colour = {regexes::edgeline.cap(1).toInt(nullptr)};
		const Vertex v_1 = vertexFromString(regexes::edgeline.cap(2));
		const Vertex v_2 = vertexFromString(regexes::edgeline.cap(3));
		return editor.append<modelobjects::Edge>(v_1, v_2, colour);
	}
	else
	{
		return nullptr;
	}
}

modelobjects::BaseObject* Parser::parseFromString(
	Model::EditContext& editor,
	const QString& line)
{
	modelobjects::Edge* edge = parseEdgeline(editor, line);
	if (edge)
	{
		return edge;
	}
	return editor.append<modelobjects::ErrorLine>(line);
}

mercurial