From 30c8d25a59a0dc0458214e61ea9d228eb41a32ae Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 14 Mar 2020 17:36:26 +0100 Subject: [PATCH 01/13] Initial parser rework --- src/CMakeLists.txt | 2 + src/sfizz/Parser.cpp | 4 +- src/sfizz/Parser.h | 4 +- src/sfizz/Synth.cpp | 2 +- src/sfizz/Synth.h | 2 +- src/sfizz/parser/Parser.cpp | 383 +++++++++++++++++++++++++++++ src/sfizz/parser/Parser.h | 109 ++++++++ src/sfizz/parser/ParserPrivate.cpp | 142 +++++++++++ src/sfizz/parser/ParserPrivate.h | 123 +++++++++ src/sfizz/parser/ParserPrivate.hpp | 50 ++++ tests/CMakeLists.txt | 4 + tests/DemoParser.cpp | 228 +++++++++++++++++ tests/DemoParser.ui | 52 ++++ tests/PlotCurve.cpp | 2 +- 14 files changed, 1100 insertions(+), 7 deletions(-) create mode 100644 src/sfizz/parser/Parser.cpp create mode 100644 src/sfizz/parser/Parser.h create mode 100644 src/sfizz/parser/ParserPrivate.cpp create mode 100644 src/sfizz/parser/ParserPrivate.h create mode 100644 src/sfizz/parser/ParserPrivate.hpp create mode 100644 tests/DemoParser.cpp create mode 100644 tests/DemoParser.ui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a7825160..139f3a8a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,8 @@ set (SFIZZ_SOURCES sfizz/Curve.cpp sfizz/Wavetables.cpp sfizz/Effects.cpp + sfizz/parser/Parser.cpp + sfizz/parser/ParserPrivate.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp diff --git a/src/sfizz/Parser.cpp b/src/sfizz/Parser.cpp index 603d684f..541ac592 100644 --- a/src/sfizz/Parser.cpp +++ b/src/sfizz/Parser.cpp @@ -19,7 +19,7 @@ void removeCommentOnLine(absl::string_view& line) line.remove_suffix(line.size() - position); } -bool sfz::Parser::loadSfzFile(const fs::path& file) +bool sfz::OldParser::loadSfzFile(const fs::path& file) { includedFiles.clear(); @@ -56,7 +56,7 @@ bool sfz::Parser::loadSfzFile(const fs::path& file) return true; } -void sfz::Parser::readSfzFile(const fs::path& fileName, std::vector& lines) noexcept +void sfz::OldParser::readSfzFile(const fs::path& fileName, std::vector& lines) noexcept { std::ifstream fileStream(fileName.c_str()); if (!fileStream) diff --git a/src/sfizz/Parser.h b/src/sfizz/Parser.h index d1a1deab..e1be5971 100644 --- a/src/sfizz/Parser.h +++ b/src/sfizz/Parser.h @@ -14,9 +14,9 @@ #include namespace sfz { -class Parser { +class OldParser { public: - virtual ~Parser() = default; + virtual ~OldParser() = default; virtual bool loadSfzFile(const fs::path& file); const std::map& getDefines() const noexcept { return defines; } const std::vector& getIncludedFiles() const noexcept { return includedFiles; } diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 997a3869..de1db07e 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -283,7 +283,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) } clear(); - auto parserReturned = sfz::Parser::loadSfzFile(file); + auto parserReturned = sfz::OldParser::loadSfzFile(file); if (!parserReturned) return false; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index fb755574..0eea55f2 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -59,7 +59,7 @@ namespace sfz { * The jack_client.cpp file contains examples of the most classical usage of the * synth and can be used as a reference. */ -class Synth : public Parser { +class Synth : public OldParser { public: /** * @brief Construct a new Synth object with no voices. If you want sound diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp new file mode 100644 index 00000000..f594e4e9 --- /dev/null +++ b/src/sfizz/parser/Parser.cpp @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "Parser.h" +#include "ParserPrivate.h" +#include "absl/memory/memory.h" + +namespace sfz { + +Parser::Parser() +{ +} + +Parser::~Parser() +{ +} + +void Parser::addDefinition(absl::string_view id, absl::string_view value) +{ + _definitions[id] = std::string(value); +} + +void Parser::parseFile(const fs::path& path) +{ + _pathsIncluded.clear(); + _lastHeader.clear(); + _errorCount = 0; + _warningCount = 0; + + if (_listener) + _listener->onParseBegin(); + + includeNewFile(path); + processTopLevel(); + + if (_listener) + _listener->onParseEnd(); +} + +void Parser::includeNewFile(const fs::path& path) +{ + fs::path fullPath = + (path.empty() || path.is_absolute()) ? path : _originalDirectory / path; + + if (!_pathsIncluded.insert(fullPath.string()).second) + return; + + auto fileReader = absl::make_unique(fullPath); + if (fileReader->hasError()) { + SourceLocation loc = fileReader->location(); + emitError({ loc, loc }, "Cannot open file for reading: " + fullPath.string()); + } + + _included.push_back(std::move(fileReader)); +} + +void Parser::processTopLevel() +{ + while (!_included.empty()) { + Reader& reader = *_included.back(); + + while (reader.skipChars(" \t\r\n") || skipComment(reader)); + + switch (reader.peekChar()) { + case Reader::kEof: + _included.pop_back(); + break; + case '#': + processDirective(); + break; + case '<': + processHeader(); + break; + default: + processOpcode(); + break; + } + } +} + +void Parser::processDirective() +{ + Reader& reader = *_included.back(); + SourceLocation start = reader.location(); + + if (reader.getChar() != '#') { + SourceLocation end = reader.location(); + emitError({ start, end }, "Expected `#` at start of directive."); + recover(); + return; + } + + std::string directive; + reader.extractWhile(&directive, isIdentifierChar); + + if (directive == "define") { + reader.skipChars(" \t"); + + std::string id; + if (!reader.extractExactChar('$') || !reader.extractWhile(&id, isIdentifierChar)) { + SourceLocation end = reader.location(); + emitError({ start, end }, "Expected $identifier after #define."); + recover(); + return; + } + + reader.skipChars(" \t"); + + std::string value; + extractToEol(reader, &value); + trimRight(value); + + addDefinition(id, value); + } + else if (directive == "include") { + reader.skipChars(" \t"); + + std::string path; + bool valid = false; + + if (reader.extractExactChar('"')) { + reader.extractWhile(&path, [](char c) { return c != '"' && c != '\r' && c != '\n'; }); + valid = reader.extractExactChar('"'); + } + + if (!valid) { + SourceLocation end = reader.location(); + emitError({ start, end }, "Expected \"file.sfz\" after #include."); + recover(); + return; + } + + includeNewFile(path); + } + else { + SourceLocation end = reader.location(); + emitError({ start, end }, "Unrecognized directive `" + directive + "`"); + recover(); + } +} + +void Parser::processHeader() +{ + Reader& reader = *_included.back(); + SourceLocation start = reader.location(); + + if (reader.getChar() != '<') { + SourceLocation end = reader.location(); + emitError({ start, end }, "Expected `<` at start of header."); + recover(); + return; + } + + std::string name; + reader.extractWhile(&name, [](char c) { + return c != '\r' && c != '\n' && c != '>'; + }); + + if (reader.peekChar() != '>') { + SourceLocation end = reader.location(); + emitError({ start, end }, "Expected `>` at end of header."); + recover(); + return; + } + reader.getChar(); + SourceLocation end = reader.location(); + + if (!isIdentifier(name)) { + emitError({ start, end }, "The header name `" + name + "` is not a valid identifier."); + recover(); + return; + } + + _lastHeader = name; + if (_listener) + _listener->onParseHeader({ start, end }, name); +} + +void Parser::processOpcode() +{ + Reader& reader = *_included.back(); + SourceLocation opcodeStart = reader.location(); + + auto isRawOpcodeNameChar = [](char c) { + return isIdentifierChar(c) || c == '$'; + }; + + std::string nameRaw; + reader.extractWhile(&nameRaw, isRawOpcodeNameChar); + + SourceLocation opcodeEnd = reader.location(); + + if (nameRaw.empty()) { + emitError({ opcodeStart, opcodeEnd }, "Expected opcode name."); + recover(); + return; + } + + if (reader.peekChar() != '=') { + emitError({ opcodeStart, opcodeEnd }, "Expected `=` after opcode name."); + recover(); + return; + } + + std::string nameExpanded = expandDollarVars({ opcodeStart, opcodeEnd }, nameRaw); + if (!isIdentifier(nameExpanded)) { + emitError({ opcodeStart, opcodeEnd }, "The opcode name `" + nameExpanded + "` is not a valid identifier."); + recover(); + return; + } + + reader.getChar(); + + SourceLocation valueStart = reader.location(); + std::string valueRaw; + extractToEol(reader, &valueRaw); + + // if another "=" character was hit, it means we read too far + size_t position = valueRaw.find('='); + if (position != valueRaw.npos) { + while (position > 0 && isRawOpcodeNameChar(valueRaw[position - 1])) + --position; + while (position > 0 && isSpaceChar(valueRaw[position - 1])) + --position; + + absl::string_view excess(&valueRaw[position], valueRaw.size() - position); + reader.putBackChars(excess); + valueRaw.resize(position); + + // ensure that we are landing back next to a space char + if (!reader.hasOneOfChars(" \t\r\n")) { + SourceLocation end = reader.location(); + emitError({ valueStart, end }, "Unexpected `=` in opcode value."); + recover(); + return; + } + } + + while (!valueRaw.empty() && isSpaceChar(valueRaw.back())) { + reader.putBackChar(valueRaw.back()); + valueRaw.pop_back(); + } + SourceLocation valueEnd = reader.location(); + + if (_lastHeader.empty()) + emitWarning({ opcodeStart, valueEnd }, "The opcode is not under any header."); + + std::string valueExpanded = expandDollarVars({ valueStart, valueEnd }, valueRaw); + if (_listener) + _listener->onParseOpcode({ opcodeStart, opcodeEnd }, { valueStart, valueEnd }, nameExpanded, valueExpanded); +} + +void Parser::emitError(const SourceRange& range, const std::string& message) +{ + ++_errorCount; + if (_listener) + _listener->onParseError(range, message); +} + +void Parser::emitWarning(const SourceRange& range, const std::string& message) +{ + ++_warningCount; + if (_listener) + _listener->onParseWarning(range, message); +} + +void Parser::recover() +{ + Reader& reader = *_included.back(); + + // skip the current line and let the parser proceed at the next + reader.skipWhile([](char c) { return c != '\n'; }); +} + +bool Parser::hasComment(Reader& reader) +{ + if (reader.peekChar() != '/') + return false; + + reader.getChar(); + if (reader.peekChar() != '/') { + reader.putBackChar('/'); + return false; + } + + return true; +} + +size_t Parser::skipComment(Reader& reader) +{ + if (!hasComment(reader)) + return 0; + + size_t count = 2; + reader.getChar(); + reader.getChar(); + + int c; + while ((c = reader.getChar()) != Reader::kEof && c != '\r' && c != '\n') + ++count; + + return count; +} + +void Parser::trimRight(std::string& text) +{ + while (!text.empty() && isSpaceChar(text.back())) + text.pop_back(); +} + +size_t Parser::extractToEol(Reader& reader, std::string* dst) +{ + return reader.extractWhile(dst, [&reader](char c) { + return c != '\r' && c != '\n' && !(c == '/' && reader.peekChar() == '/'); + }); +} + +std::string Parser::expandDollarVars(const SourceRange& range, absl::string_view src) +{ + std::string dst; + dst.reserve(2 * src.size()); + + size_t i = 0; + size_t n = src.size(); + while (i < n) { + char c = src[i++]; + + if (c != '$') + dst.push_back(c); + else { + std::string name; + name.reserve(64); + + while (i < n && isIdentifierChar(src[i])) + name.push_back(src[i++]); + + if (name.empty()) { + emitWarning(range, "Expected variable name after $."); + continue; + } + + auto it = _definitions.find(name); + if (it == _definitions.end()) { + emitWarning(range, "The variable `" + name + "` is not defined."); + continue; + } + + dst.append(it->second); + } + } + + return dst; +} + +bool Parser::isIdentifierChar(char c) +{ + return c == '_' || + (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9'); +} + +bool Parser::isSpaceChar(char c) +{ + return c == ' ' || c == '\t'; +} + +bool Parser::isIdentifier(absl::string_view s) +{ + if (s.empty()) + return false; + + for (char c : s) { + if (!isIdentifierChar(c)) + return false; + } + + return true; +} + +} // namespace sfz diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h new file mode 100644 index 00000000..8f97ae41 --- /dev/null +++ b/src/sfizz/parser/Parser.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "ghc/fs_std.hpp" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include +#include + +namespace sfz { + +class Reader; +struct SourceLocation; +struct SourceRange; + +/** + * @brief Context-dependent parser for SFZ files + */ +class Parser { +public: + Parser(); + ~Parser(); + + void addDefinition(absl::string_view id, absl::string_view value); + void parseFile(const fs::path& path); + + size_t getErrorCount() const noexcept { return _errorCount; } + size_t getWarningCount() const noexcept { return _warningCount; } + + class Listener { + public: + virtual void onParseBegin() = 0; + virtual void onParseEnd() = 0; + virtual void onParseHeader(const SourceRange& range, const std::string& header) = 0; + virtual void onParseOpcode(const SourceRange& rangeOpcode, const SourceRange& rangeValue, const std::string& name, const std::string& value) = 0; + virtual void onParseError(const SourceRange& range, const std::string& message) = 0; + virtual void onParseWarning(const SourceRange& range, const std::string& message) = 0; + }; + + void setListener(Listener* listener) noexcept { _listener = listener; } + +private: + void includeNewFile(const fs::path& path); + void processTopLevel(); + void processDirective(); + void processHeader(); + void processOpcode(); + + // errors and warnings + void emitError(const SourceRange& range, const std::string& message); + void emitWarning(const SourceRange& range, const std::string& message); + + // recover after error + void recover(); + + // helpers + static bool hasComment(Reader& reader); + static size_t skipComment(Reader& reader); + static void trimRight(std::string& text); + static size_t extractToEol(Reader& reader, std::string* dst); // ignores comment + std::string expandDollarVars(const SourceRange& range, absl::string_view src); + + // predicates + static bool isIdentifierChar(char c); + static bool isSpaceChar(char c); + static bool isIdentifier(absl::string_view s); + +private: + Listener* _listener = nullptr; + + fs::path _originalDirectory { fs::current_path() }; + absl::flat_hash_map _definitions; + + // a current list of files included, last one at the back + std::vector> _included; + + // recursive include guard + absl::flat_hash_set _pathsIncluded; + + // parsing state + std::string _lastHeader; + + // errors and warnings + size_t _errorCount = 0; + size_t _warningCount = 0; +}; + +/** + * @brief Source file location for errors and warnings. + */ +struct SourceLocation { + std::shared_ptr filePath; + size_t lineNumber = 0; + size_t columnNumber = 0; +}; + +/** + * @brief Range of source file. + */ +struct SourceRange { + SourceLocation start; + SourceLocation end; +}; + +} // namespace sfz diff --git a/src/sfizz/parser/ParserPrivate.cpp b/src/sfizz/parser/ParserPrivate.cpp new file mode 100644 index 00000000..0db8423d --- /dev/null +++ b/src/sfizz/parser/ParserPrivate.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ParserPrivate.h" + +namespace sfz { + +Reader::Reader(const fs::path& filePath) +{ + _accum.reserve(256); + _loc.filePath = std::make_shared(filePath); + _lineNumColumns.reserve(256); +} + +int Reader::getChar() +{ + int byte; + + if (_accum.empty()) + byte = getNextStreamByte(); + else { + byte = static_cast(_accum.back()); + _accum.pop_back(); + } + + if (byte != kEof) + updateSourceLocationAdding(byte); + + return byte; +} + +int Reader::peekChar() +{ + int byte; + + if (_accum.empty()) { + byte = getChar(); + putBackChar(byte); + } + else + byte = static_cast(_accum.back()); + + return byte; +} + +bool Reader::extractExactChar(char c) +{ + int next = peekChar(); + + if (next == kEof || next != static_cast(c)) + return false; + + getChar(); + return true; +} + +void Reader::putBackChar(int c) +{ + if (c == kEof) + return; + + char c8bit = static_cast(c); + putBackChars(absl::string_view(&c8bit, 1)); +} + +void Reader::putBackChars(absl::string_view characters) +{ + _accum.append(characters.rbegin(), characters.rend()); + + for (size_t i = characters.size(); i-- > 0;) + updateSourceLocationRemoving(static_cast(characters[i])); +} + +size_t Reader::skipChars(absl::string_view chars) +{ + size_t count = 0; + + while (chars.find(static_cast(peekChar())) != chars.npos) { + getChar(); + ++count; + } + + return count; +} + +bool Reader::hasEof() +{ + return peekChar() == kEof; +} + +bool Reader::hasOneOfChars(absl::string_view chars) +{ + int c = peekChar(); + if (c == kEof) + return false; + + return chars.find(static_cast(c)) != chars.npos; +} + +void Reader::updateSourceLocationAdding(int byte) +{ + if (byte != '\n') + _loc.columnNumber += 1; + else { + _lineNumColumns.push_back(_loc.columnNumber); + _loc.lineNumber += 1; + _loc.columnNumber = 0; + } +} + +void Reader::updateSourceLocationRemoving(int byte) +{ + if (byte != '\n') + _loc.columnNumber -= 1; + else { + _loc.lineNumber -= 1; + _loc.columnNumber = _lineNumColumns[_loc.lineNumber]; + _lineNumColumns.pop_back(); + } +} + +//------------------------------------------------------------------------------ + +FileReader::FileReader(const fs::path& filePath) + : Reader(filePath), _fileStream(filePath) +{ +} + +bool FileReader::hasError() +{ + return !_fileStream.is_open() || _fileStream.bad(); +} + +int FileReader::getNextStreamByte() +{ + return _fileStream.get(); +} + +} // namespace sfz diff --git a/src/sfizz/parser/ParserPrivate.h b/src/sfizz/parser/ParserPrivate.h new file mode 100644 index 00000000..39aeca8e --- /dev/null +++ b/src/sfizz/parser/ParserPrivate.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Parser.h" +#include "ghc/fs_std.hpp" +#include "absl/strings/string_view.h" +#include + +namespace sfz { + +/** + * @brief Utility to extract characters and strings from a source of any kind. + */ +class Reader { +public: + explicit Reader(const fs::path& filePath); + virtual ~Reader() {} + + /** + * @brief Get the current source location. + */ + const SourceLocation& location() const { return _loc; } + + /** + * @brief Value of the end-of-file marker. + */ + static constexpr int kEof = std::char_traits::eof(); + + /** + * @brief Extract the next character. + */ + int getChar(); + + /** + * @brief Get the next character without extracting it. + */ + int peekChar(); + + /** + * @brief Put a previously extracted character back into the reader. + */ + void putBackChar(int c); + + /** + * @brief Put some previously extracted characters back into the reader. + */ + void putBackChars(absl::string_view characters); + + /** + * @brief Extract as long as a predicate holds on the next character. + */ + template size_t extractWhile(std::string* dst, const P& pred); + + /** + * @brief Extract until as a predicate does not hold on the next character. + */ + template size_t extractUntil(std::string* dst, const P& pred); + + /** + * @brief Extract a character if it is equal to the expected value. + */ + bool extractExactChar(char c); + + /** + * @brief Skip characters which belong to a given set + */ + size_t skipChars(absl::string_view chars); + + /** + * @brief Skip as long as a predicate holds on the next character. + */ + template size_t skipWhile(const P& pred); + + /** + * @brief Skip until as a predicate does not hold on the next character. + */ + template size_t skipUntil(const P& pred); + + /** + * @brief Check if the reader has no more characters. + */ + bool hasEof(); + + /** + * @brief Check if the reader has one of the following characters next. + */ + bool hasOneOfChars(absl::string_view chars); + +protected: + virtual int getNextStreamByte() = 0; + +private: + void updateSourceLocationAdding(int byte); + void updateSourceLocationRemoving(int byte); + +private: + std::string _accum; // new characters at the front, old at the back + SourceLocation _loc; + std::vector _lineNumColumns; +}; + +/** + * @brief File-based version of Reader. + */ +class FileReader : public Reader { +public: + explicit FileReader(const fs::path& filePath); + bool hasError(); + +protected: + int getNextStreamByte() override; + +private: + fs::ifstream _fileStream; +}; + +} // namespace sfz + +#include "ParserPrivate.hpp" diff --git a/src/sfizz/parser/ParserPrivate.hpp b/src/sfizz/parser/ParserPrivate.hpp new file mode 100644 index 00000000..c7ba53e1 --- /dev/null +++ b/src/sfizz/parser/ParserPrivate.hpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "ParserPrivate.h" + +namespace sfz { + +template +size_t Reader::extractWhile(std::string* dst, const P& pred) +{ + int byte; + size_t count = 0; + bool more = true; + + while (more && (byte = getChar()) != EOF) { + more = pred(static_cast(byte)); + if (!more) + putBackChar(byte); + else { + if (dst) + dst->push_back(static_cast(byte)); + ++count; + } + } + + return count; +} + +template +size_t Reader::extractUntil(std::string* dst, const P& pred) +{ + return extractWhile(dst, [&pred](char c) -> bool { return !pred(c); }); +} + +template +size_t Reader::skipWhile(const P& pred) +{ + return extractWhile(nullptr, pred); +} + +template +size_t Reader::skipUntil(const P& pred) +{ + return extractUntil(nullptr, pred); +} + +} // namespace sfz diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 598e58ea..29cd312e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -55,6 +55,10 @@ if(JACK_FOUND AND TARGET Qt5::Widgets) target_include_directories(sfizz_demo_wavetables PRIVATE ${JACK_INCLUDE_DIRS}) target_link_libraries(sfizz_demo_wavetables PRIVATE sfizz::sfizz Qt5::Widgets ${JACK_LIBRARIES}) set_target_properties(sfizz_demo_wavetables PROPERTIES AUTOUIC ON) + + add_executable(sfizz_demo_parser DemoParser.cpp) + target_link_libraries(sfizz_demo_parser PRIVATE sfizz::sfizz Qt5::Widgets) + set_target_properties(sfizz_demo_parser PROPERTIES AUTOUIC ON) endif() add_executable(eq_apply EQ.cpp) diff --git a/tests/DemoParser.cpp b/tests/DemoParser.cpp new file mode 100644 index 00000000..fa0c5078 --- /dev/null +++ b/tests/DemoParser.cpp @@ -0,0 +1,228 @@ +#include "ui_DemoParser.h" +#include "sfizz/parser/Parser.h" +#include +#include +#include +#include +#include +#include +#include + +class Application : public QApplication, public sfz::Parser::Listener { +public: + Application(int& argc, char *argv[]) : QApplication(argc, argv) {} + void init(); + +private: + void requestParseCheck(); + void runParseCheck(); + +private: + void onParseBegin() override; + void onParseEnd() override; + void onParseHeader(const sfz::SourceRange& range, const std::string& header) override; + void onParseOpcode(const sfz::SourceRange& rangeOpcode, const sfz::SourceRange& rangeValue, const std::string& name, const std::string& value) override; + void onParseError(const sfz::SourceRange& range, const std::string& message) override; + void onParseWarning(const sfz::SourceRange& range, const std::string& message) override; + + static QTextCursor selectSourceRange(QTextDocument* doc, const sfz::SourceRange& range); + +private: + sfz::Parser _parser; + Ui::MainWindow _ui; + QTimer* _recheckTimer = nullptr; + bool _blockTextChanged = false; +}; + +static const char defaultSfzText[] = R"SFZ( +//----------------------------------------------------------------------------// +// This is a SFZ test file with many problems. // +//----------------------------------------------------------------------------// + +// opcode without header +not_in_header=on // warning + +// invalid headers +<> // empty + // bad identifier + + +sample=*sine key=69 +sample=My Directory/My Wave.wav // path with spaces +sample=My Directory/My Wave.wav key=69 // path with spaces, and other opcode following +sample=Foo=Bar.wav // path invalid: it cannot contain the '=' sign + +#include "FileWhichDoesNotExist.sfz" + +// malformed includes +#include "MyFileWhichDoesNotExist1.sfz +#include MyFileWhichDoesNotExist1.sfz" + +// #define with some bad variable names +#define $foo 1234 +#define Foo 1234 +#define $ 1234 + +// #define with empty expansion, accepted +#define $foo + +// expansion +abc$foo=1 +abcdef=$foo + +// expansion of undefined variables +abc$toto=1 +abcdef=$tata + +// opcode name which expands to invalid identifier +$titi=1 +)SFZ"; + +void Application::init() +{ + QMainWindow* window = new QMainWindow; + _ui.setupUi(window); + + _recheckTimer = new QTimer; + _recheckTimer->setSingleShot(true); + _recheckTimer->setInterval(200); + connect( + _recheckTimer, &QTimer::timeout, + this, [this]() { runParseCheck(); }); + + _ui.sfzEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + _ui.sfzEdit->setPlainText(QString::fromLatin1(defaultSfzText)); + connect( + _ui.sfzEdit, &QTextEdit::textChanged, + this, [this]() { if (!_blockTextChanged) requestParseCheck(); }); + + _ui.messageTable->setColumnCount(3); + _ui.messageTable->setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Line") << tr("Message")); + _ui.messageTable->horizontalHeader()->setStretchLastSection(true); + + window->show(); + + _parser.setListener(this); + requestParseCheck(); +} + +void Application::requestParseCheck() +{ + _recheckTimer->start(); +} + +void Application::runParseCheck() +{ + QByteArray code = _ui.sfzEdit->toPlainText().toLatin1(); + + QTemporaryFile temp(QDir::tempPath() + "/parseXXXXXX.sfz"); + temp.open(); + temp.write(code); + temp.flush(); + + _blockTextChanged = true; + _parser.parseFile(temp.fileName().toStdString()); + _blockTextChanged = false; +} + +void Application::onParseBegin() +{ + QTextDocument* doc = _ui.sfzEdit->document(); + QTextCursor cur(doc); + cur.movePosition(QTextCursor::Start); + cur.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); + QTextCharFormat cfmt; + cur.setCharFormat(cfmt); + + QTableWidget* t = _ui.messageTable; + while (t->rowCount() > 0) + t->removeRow(t->rowCount() - 1); +} + +void Application::onParseEnd() +{ +} + +void Application::onParseHeader(const sfz::SourceRange& range, const std::string& header) +{ + (void)header; + + QTextDocument* doc = _ui.sfzEdit->document(); + QTextCursor cur = selectSourceRange(doc, range); + QTextCharFormat cfmt; + cfmt.setForeground(QColor::fromRgb(0x4E9A06)); + cur.mergeCharFormat(cfmt); +} + +void Application::onParseOpcode(const sfz::SourceRange& rangeOpcode, const sfz::SourceRange& rangeValue, const std::string& name, const std::string& value) +{ + (void)name; + (void)value; + + QTextDocument* doc = _ui.sfzEdit->document(); + QTextCursor curOpcode = selectSourceRange(doc, rangeOpcode); + QTextCursor curValue = selectSourceRange(doc, rangeValue); + QTextCharFormat cfmtOpcode; + cfmtOpcode.setForeground(QColor::fromRgb(0x75507B)); + QTextCharFormat cfmtValue; + cfmtValue.setForeground(QColor::fromRgb(0x3465A4)); + curOpcode.mergeCharFormat(cfmtOpcode); + curValue.mergeCharFormat(cfmtValue); +} + +void Application::onParseError(const sfz::SourceRange& range, const std::string& message) +{ + QTableWidget* t = _ui.messageTable; + int row = t->rowCount(); + t->insertRow(row); + + // QString path = QString::fromStdString(*range.start.filePath); + // QString file = QFileInfo(path).fileName(); + t->setItem(row, 0, new QTableWidgetItem(tr("Error"))); + t->setItem(row, 1, new QTableWidgetItem(QString::number(range.start.lineNumber))); + t->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(message))); + + QTextDocument* doc = _ui.sfzEdit->document(); + QTextCursor cur = selectSourceRange(doc, range); + QTextCharFormat cfmt; + cfmt.setUnderlineStyle(QTextCharFormat::SingleUnderline); + cfmt.setUnderlineColor(Qt::red); + cur.mergeCharFormat(cfmt); +} + +void Application::onParseWarning(const sfz::SourceRange& range, const std::string& message) +{ + QTableWidget* t = _ui.messageTable; + int row = t->rowCount(); + t->insertRow(row); + + // QString path = QString::fromStdString(*range.start.filePath); + // QString file = QFileInfo(path).fileName(); + t->setItem(row, 0, new QTableWidgetItem(tr("Warning"))); + t->setItem(row, 1, new QTableWidgetItem(QString::number(range.start.lineNumber))); + t->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(message))); + + QTextDocument* doc = _ui.sfzEdit->document(); + QTextCursor cur = selectSourceRange(doc, range); + QTextCharFormat cfmt; + cfmt.setUnderlineStyle(QTextCharFormat::SingleUnderline); + cfmt.setUnderlineColor(Qt::gray); + cur.mergeCharFormat(cfmt); +} + +QTextCursor Application::selectSourceRange(QTextDocument* doc, const sfz::SourceRange& range) +{ + QTextCursor cur(doc->findBlockByLineNumber(range.start.lineNumber)); + cur.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, range.start.columnNumber); + cur.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, range.end.lineNumber - range.start.lineNumber); + cur.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); + cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, range.end.columnNumber); + return cur; +} + +int main(int argc, char* argv[]) +{ + Application app(argc, argv); + app.init(); + return app.exec(); +} diff --git a/tests/DemoParser.ui b/tests/DemoParser.ui new file mode 100644 index 00000000..6198fd1a --- /dev/null +++ b/tests/DemoParser.ui @@ -0,0 +1,52 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + Sfizz parser demo + + + + + + + Qt::Vertical + + + + + Messages + + + + + + + + + + + + + + + 0 + 0 + 800 + 20 + + + + + + + + diff --git a/tests/PlotCurve.cpp b/tests/PlotCurve.cpp index 5ca42c28..c1846ba2 100644 --- a/tests/PlotCurve.cpp +++ b/tests/PlotCurve.cpp @@ -36,7 +36,7 @@ static void usage() /** * @brief Parser which extracts the configuration of curves */ -class CurveParser : public sfz::Parser { +class CurveParser : public sfz::OldParser { public: explicit CurveParser(sfz::CurveSet& curveSet) : curveSet(curveSet) From 9019ae3aef935ab7ed834fa7a9f0ec39bf40ff31 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 17 Mar 2020 18:37:20 +0100 Subject: [PATCH 02/13] Some parser fixes --- src/CMakeLists.txt | 8 ++++---- src/sfizz/parser/ParserPrivate.h | 1 + tests/CMakeLists.txt | 2 +- tests/DemoParser.cpp | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 139f3a8a..58b366e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,8 +19,6 @@ set (SFIZZ_SOURCES sfizz/Curve.cpp sfizz/Wavetables.cpp sfizz/Effects.cpp - sfizz/parser/Parser.cpp - sfizz/parser/ParserPrivate.cpp sfizz/effects/Nothing.cpp sfizz/effects/Filter.cpp sfizz/effects/Eq.cpp @@ -31,10 +29,12 @@ include (SfizzSIMDSourceFiles) # Parser core library add_library (sfizz_parser STATIC) -target_sources (sfizz_parser PRIVATE sfizz/Parser.cpp sfizz/Opcode.cpp sfizz/SfzHelpers.cpp) +target_sources (sfizz_parser PRIVATE + sfizz/Parser.cpp sfizz/Opcode.cpp sfizz/SfzHelpers.cpp + sfizz/parser/Parser.cpp sfizz/parser/ParserPrivate.cpp) target_include_directories (sfizz_parser PUBLIC sfizz) target_include_directories (sfizz_parser PUBLIC external) -target_link_libraries (sfizz_parser PUBLIC absl::strings) +target_link_libraries (sfizz_parser PUBLIC absl::strings PRIVATE absl::flat_hash_map) # Sfizz static library add_library(sfizz_static STATIC) diff --git a/src/sfizz/parser/ParserPrivate.h b/src/sfizz/parser/ParserPrivate.h index 39aeca8e..a0c2fecd 100644 --- a/src/sfizz/parser/ParserPrivate.h +++ b/src/sfizz/parser/ParserPrivate.h @@ -9,6 +9,7 @@ #include "ghc/fs_std.hpp" #include "absl/strings/string_view.h" #include +#include namespace sfz { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 29cd312e..30db1c1e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -57,7 +57,7 @@ if(JACK_FOUND AND TARGET Qt5::Widgets) set_target_properties(sfizz_demo_wavetables PROPERTIES AUTOUIC ON) add_executable(sfizz_demo_parser DemoParser.cpp) - target_link_libraries(sfizz_demo_parser PRIVATE sfizz::sfizz Qt5::Widgets) + target_link_libraries(sfizz_demo_parser PRIVATE sfizz_parser Qt5::Widgets) set_target_properties(sfizz_demo_parser PROPERTIES AUTOUIC ON) endif() diff --git a/tests/DemoParser.cpp b/tests/DemoParser.cpp index fa0c5078..42893a76 100644 --- a/tests/DemoParser.cpp +++ b/tests/DemoParser.cpp @@ -1,5 +1,5 @@ #include "ui_DemoParser.h" -#include "sfizz/parser/Parser.h" +#include "parser/Parser.h" #include #include #include From c03345e95045ac39406faecf7ecd58248b3301ee Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 17 Mar 2020 22:14:35 +0100 Subject: [PATCH 03/13] Adapt the newer parser to Sfizz --- clients/jack_client.cpp | 6 +++--- src/sfizz/Opcode.cpp | 6 ++---- src/sfizz/Opcode.h | 4 ++-- src/sfizz/Synth.cpp | 25 ++++++++++++++++------ src/sfizz/Synth.h | 32 ++++++++++++++++++++++++++--- src/sfizz/parser/Parser.cpp | 38 ++++++++++++++++++++++++++++++---- src/sfizz/parser/Parser.h | 41 +++++++++++++++++++++++++++++-------- tests/FilesT.cpp | 11 ++++++---- tests/PlotCurve.cpp | 17 ++++++++------- 9 files changed, 138 insertions(+), 42 deletions(-) diff --git a/clients/jack_client.cpp b/clients/jack_client.cpp index 599e0db3..771241e9 100644 --- a/clients/jack_client.cpp +++ b/clients/jack_client.cpp @@ -210,11 +210,11 @@ int main(int argc, char** argv) std::cout << "\tPreloadedSamples: " << synth.getNumPreloadedSamples() << '\n'; std::cout << "==========" << '\n'; std::cout << "Included files:" << '\n'; - for (auto& file : synth.getIncludedFiles()) - std::cout << '\t' << file.string() << '\n'; + for (auto& file : synth.getParser().getIncludedFiles()) + std::cout << '\t' << file << '\n'; std::cout << "==========" << '\n'; std::cout << "Defines:" << '\n'; - for (auto& define : synth.getDefines()) + for (auto& define : synth.getParser().getDefines()) std::cout << '\t' << define.first << '=' << define.second << '\n'; std::cout << "==========" << '\n'; std::cout << "Unknown opcodes:"; diff --git a/src/sfizz/Opcode.cpp b/src/sfizz/Opcode.cpp index cc022c11..fbe8bcc5 100644 --- a/src/sfizz/Opcode.cpp +++ b/src/sfizz/Opcode.cpp @@ -9,11 +9,9 @@ #include sfz::Opcode::Opcode(absl::string_view inputOpcode, absl::string_view inputValue) - : opcode(inputOpcode) - , value(inputValue) + : opcode(trim(inputOpcode)) + , value(trim(inputValue)) { - trimInPlace(value); - trimInPlace(opcode); size_t nextCharIndex { 0 }; int parameterPosition { 0 }; auto nextNumIndex = opcode.find_first_of("1234567890"); diff --git a/src/sfizz/Opcode.h b/src/sfizz/Opcode.h index c510f5a8..e4ff6c26 100644 --- a/src/sfizz/Opcode.h +++ b/src/sfizz/Opcode.h @@ -28,8 +28,8 @@ namespace sfz { struct Opcode { Opcode() = delete; Opcode(absl::string_view inputOpcode, absl::string_view inputValue); - absl::string_view opcode {}; - absl::string_view value {}; + std::string opcode {}; + std::string value {}; uint64_t lettersOnlyHash { Fnv1aBasis }; // This is to handle the integer parameters of some opcodes std::vector parameters; diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index de1db07e..1f0ab2eb 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -28,6 +28,8 @@ sfz::Synth::Synth() sfz::Synth::Synth(int numVoices) { + parser.setListener(this); + effectFactory.registerStandardEffectTypes(); effectBuses.reserve(5); // sufficient room for main and fx1-4 @@ -48,7 +50,7 @@ sfz::Synth::~Synth() resources.filePool.emptyFileLoadingQueues(); } -void sfz::Synth::callback(absl::string_view header, const std::vector& members) +void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector& members) { switch (hash(header)) { case hash("global"): @@ -81,6 +83,16 @@ void sfz::Synth::callback(absl::string_view header, const std::vector& m } } +void sfz::Synth::onParseError(const SourceRange& range, const std::string& message) +{ + std::cerr << "Parse error L" << range.start.lineNumber << ": " << message << '\n'; +} + +void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& message) +{ + std::cerr << "Parse warning L" << range.start.lineNumber << ": " << message << '\n'; +} + void sfz::Synth::buildRegion(const std::vector& regionOpcodes) { auto lastRegion = absl::make_unique(resources.midiState, defaultPath); @@ -283,14 +295,15 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) } clear(); - auto parserReturned = sfz::OldParser::loadSfzFile(file); - if (!parserReturned) + + parser.parseFile(file); + if (parser.getErrorCount() > 0) return false; if (regions.empty()) return false; - resources.filePool.setRootDirectory(this->originalDirectory); + resources.filePool.setRootDirectory(parser.originalDirectory()); auto currentRegion = regions.begin(); auto lastRegion = regions.rbegin(); @@ -403,7 +416,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) voice->setMaxEQsPerVoice(maxEQs); } - return parserReturned; + return true; } sfz::Voice* sfz::Synth::findFreeVoice() noexcept @@ -939,7 +952,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept fs::file_time_type sfz::Synth::checkModificationTime() { auto returnedTime = modificationTime; - for (const auto& file: getIncludedFiles()) { + for (const auto& file: parser.getIncludedFiles()) { const auto fileTime = fs::last_write_time(file); if (returnedTime < fileTime) returnedTime = fileTime; diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index 0eea55f2..f42d67e3 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -14,6 +14,7 @@ #include "LeakDetector.h" #include "MidiState.h" #include "AudioSpan.h" +#include "parser/Parser.h" #include "absl/types/span.h" #include #include @@ -59,7 +60,7 @@ namespace sfz { * The jack_client.cpp file contains examples of the most classical usage of the * synth and can be used as a reference. */ -class Synth : public OldParser { +class Synth final : public Parser::Listener { public: /** * @brief Construct a new Synth object with no voices. If you want sound @@ -88,7 +89,7 @@ public: * @return true * @return false if the file was not found or no regions were loaded. */ - bool loadSfzFile(const fs::path& file) final; + bool loadSfzFile(const fs::path& file); /** * @brief Get the current number of regions loaded * @@ -373,6 +374,20 @@ public: * */ void allSoundOff() noexcept; + + /** + * @brief Get the parser. + * + * @return A reference to the parser. + */ + Parser& getParser() noexcept { return parser; } + /** + * @brief Get the parser. + * + * @return A reference to the parser. + */ + const Parser& getParser() const noexcept { return parser; } + protected: /** * @brief The parser callback; this is called by the parent object each time @@ -382,7 +397,17 @@ protected: * @param header the header for the set of opcodes * @param members the opcode members */ - void callback(absl::string_view header, const std::vector& members) final; + void onParseFullBlock(const std::string& header, const std::vector& members) override; + + /** + * @brief The parser callback when an error occurs. + */ + void onParseError(const SourceRange& range, const std::string& message) override; + + /** + * @brief The parser callback when a warning occurs. + */ + void onParseWarning(const SourceRange& range, const std::string& message) override; private: /** @@ -505,6 +530,7 @@ private: Duration dispatchDuration { 0 }; + Parser parser; fs::file_time_type modificationTime { }; LEAK_DETECTOR(Synth); diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index f594e4e9..7efb4f62 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -26,7 +26,8 @@ void Parser::addDefinition(absl::string_view id, absl::string_view value) void Parser::parseFile(const fs::path& path) { _pathsIncluded.clear(); - _lastHeader.clear(); + _currentHeader.reset(); + _currentOpcodes.clear(); _errorCount = 0; _warningCount = 0; @@ -35,6 +36,7 @@ void Parser::parseFile(const fs::path& path) includeNewFile(path); processTopLevel(); + flushCurrentHeader(); if (_listener) _listener->onParseEnd(); @@ -45,15 +47,28 @@ void Parser::includeNewFile(const fs::path& path) fs::path fullPath = (path.empty() || path.is_absolute()) ? path : _originalDirectory / path; - if (!_pathsIncluded.insert(fullPath.string()).second) + if (_pathsIncluded.empty()) + _originalDirectory = fullPath.parent_path(); + else if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) { + if (_recursiveIncludeGuardEnabled) + return; + } + + if (_included.size() == _maxIncludeDepth) { + SourceLocation loc; + loc.filePath = std::make_shared(fullPath); + emitError({ loc, loc }, "Exceeded maximum include depth (" + std::to_string(_maxIncludeDepth) + ")"); return; + } auto fileReader = absl::make_unique(fullPath); if (fileReader->hasError()) { SourceLocation loc = fileReader->location(); emitError({ loc, loc }, "Cannot open file for reading: " + fullPath.string()); + return; } + _pathsIncluded.insert(fullPath.string()); _included.push_back(std::move(fileReader)); } @@ -174,7 +189,9 @@ void Parser::processHeader() return; } - _lastHeader = name; + flushCurrentHeader(); + + _currentHeader = name; if (_listener) _listener->onParseHeader({ start, end }, name); } @@ -245,10 +262,12 @@ void Parser::processOpcode() } SourceLocation valueEnd = reader.location(); - if (_lastHeader.empty()) + if (!_currentHeader) emitWarning({ opcodeStart, valueEnd }, "The opcode is not under any header."); std::string valueExpanded = expandDollarVars({ valueStart, valueEnd }, valueRaw); + _currentOpcodes.emplace_back(nameExpanded, valueExpanded); + if (_listener) _listener->onParseOpcode({ opcodeStart, opcodeEnd }, { valueStart, valueEnd }, nameExpanded, valueExpanded); } @@ -275,6 +294,17 @@ void Parser::recover() reader.skipWhile([](char c) { return c != '\n'; }); } +void Parser::flushCurrentHeader() +{ + if (_currentHeader) { + if (_listener) + _listener->onParseFullBlock(*_currentHeader, _currentOpcodes); + _currentHeader.reset(); + } + + _currentOpcodes.clear(); +} + bool Parser::hasComment(Reader& reader) { if (reader.peekChar() != '/') diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h index 8f97ae41..cdcb50cd 100644 --- a/src/sfizz/parser/Parser.h +++ b/src/sfizz/parser/Parser.h @@ -5,7 +5,9 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once +#include "../Opcode.h" #include "ghc/fs_std.hpp" +#include "absl/types/optional.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include @@ -28,17 +30,32 @@ public: void addDefinition(absl::string_view id, absl::string_view value); void parseFile(const fs::path& path); + void setRecursiveIncludeGuardEnabled(bool en) { _recursiveIncludeGuardEnabled = en; } + void setMaximumIncludeDepth(size_t depth) { _maxIncludeDepth = depth; } + + const fs::path& originalDirectory() const noexcept { return _originalDirectory; } + + typedef absl::flat_hash_set IncludeFileSet; + typedef absl::flat_hash_map DefinitionSet; + + const IncludeFileSet& getIncludedFiles() const noexcept { return _pathsIncluded; } + const DefinitionSet& getDefines() const noexcept { return _definitions; } + size_t getErrorCount() const noexcept { return _errorCount; } size_t getWarningCount() const noexcept { return _warningCount; } class Listener { public: - virtual void onParseBegin() = 0; - virtual void onParseEnd() = 0; - virtual void onParseHeader(const SourceRange& range, const std::string& header) = 0; - virtual void onParseOpcode(const SourceRange& rangeOpcode, const SourceRange& rangeValue, const std::string& name, const std::string& value) = 0; - virtual void onParseError(const SourceRange& range, const std::string& message) = 0; - virtual void onParseWarning(const SourceRange& range, const std::string& message) = 0; + // low-level parsing + virtual void onParseBegin() {} + virtual void onParseEnd() {} + virtual void onParseHeader(const SourceRange& /*range*/, const std::string& /*header*/) {} + virtual void onParseOpcode(const SourceRange& /*rangeOpcode*/, const SourceRange& /*rangeValue*/, const std::string& /*name*/, const std::string& /*value*/) {} + virtual void onParseError(const SourceRange& /*range*/, const std::string& /*message*/) {} + virtual void onParseWarning(const SourceRange& /*range*/, const std::string& /*message*/) {} + + // high-level parsing + virtual void onParseFullBlock(const std::string& /*header*/, const std::vector& /*opcodes*/) {} }; void setListener(Listener* listener) noexcept { _listener = listener; } @@ -57,6 +74,9 @@ private: // recover after error void recover(); + // state handling + void flushCurrentHeader(); + // helpers static bool hasComment(Reader& reader); static size_t skipComment(Reader& reader); @@ -73,16 +93,19 @@ private: Listener* _listener = nullptr; fs::path _originalDirectory { fs::current_path() }; - absl::flat_hash_map _definitions; + DefinitionSet _definitions; // a current list of files included, last one at the back std::vector> _included; // recursive include guard - absl::flat_hash_set _pathsIncluded; + size_t _maxIncludeDepth = 32; + bool _recursiveIncludeGuardEnabled = false; + IncludeFileSet _pathsIncluded; // parsing state - std::string _lastHeader; + absl::optional _currentHeader; + std::vector _currentOpcodes; // errors and warnings size_t _errorCount = 0; diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index b397d154..6284ec6c 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -98,7 +98,8 @@ TEST_CASE("[Files] Subdir include Win") TEST_CASE("[Files] Recursive include (with include guard)") { sfz::Synth synth; - synth.enableRecursiveIncludeGuard(); + sfz::Parser& parser = synth.getParser(); + parser.setRecursiveIncludeGuardEnabled(true); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_recursive.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sample == "dummy_recursive2.wav"); @@ -108,7 +109,8 @@ TEST_CASE("[Files] Recursive include (with include guard)") TEST_CASE("[Files] Include loops (with include guard)") { sfz::Synth synth; - synth.enableRecursiveIncludeGuard(); + sfz::Parser& parser = synth.getParser(); + parser.setRecursiveIncludeGuardEnabled(true); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_loop.sfz"); REQUIRE(synth.getNumRegions() == 2); REQUIRE(synth.getRegionView(0)->sample == "dummy_loop2.wav"); @@ -502,8 +504,9 @@ TEST_CASE("[Files] Case sentitiveness") TEST_CASE("[Files] Empty file") { sfz::Synth synth; + sfz::Parser& parser = synth.getParser(); REQUIRE(!synth.loadSfzFile("")); - REQUIRE(synth.getIncludedFiles().empty()); + REQUIRE(parser.getIncludedFiles().empty()); REQUIRE(!synth.loadSfzFile({})); - REQUIRE(synth.getIncludedFiles().empty()); + REQUIRE(parser.getIncludedFiles().empty()); } diff --git a/tests/PlotCurve.cpp b/tests/PlotCurve.cpp index c1846ba2..954a7946 100644 --- a/tests/PlotCurve.cpp +++ b/tests/PlotCurve.cpp @@ -16,7 +16,7 @@ */ #include "sfizz/Curve.h" -#include "sfizz/Parser.h" +#include "sfizz/parser/Parser.h" #include "absl/strings/numbers.h" #include "absl/types/span.h" #include @@ -34,16 +34,16 @@ static void usage() } /** - * @brief Parser which extracts the configuration of curves + * @brief Parser listener which extracts the configuration of curves */ -class CurveParser : public sfz::OldParser { +class CurveParserListener : public sfz::Parser::Listener { public: - explicit CurveParser(sfz::CurveSet& curveSet) + explicit CurveParserListener(sfz::CurveSet& curveSet) : curveSet(curveSet) { } - void callback(absl::string_view header, const std::vector& members) override + void onParseFullBlock(const std::string& header, const std::vector& members) override { if (header == "curve") curveSet.addCurveFromHeader(members); @@ -73,8 +73,11 @@ int main(int argc, char* argv[]) sfz::CurveSet curveSet = sfz::CurveSet::createPredefined(); if (!filePath.empty()) { - CurveParser parser(curveSet); - if (!parser.loadSfzFile(filePath)) { + sfz::Parser parser; + CurveParserListener listener(curveSet); + parser.setListener(&listener); + parser.parseFile(filePath); + if (parser.getErrorCount() > 0) { std::cerr << "Cannot load SFZ: " << filePath << "\n"; return 1; } From 972dbe06ea3f2bee442fdbdb93b848cb0c797881 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Tue, 17 Mar 2020 22:45:03 +0100 Subject: [PATCH 04/13] Fix the backslash include paths --- src/sfizz/parser/Parser.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 7efb4f62..8007e40c 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -148,6 +148,7 @@ void Parser::processDirective() return; } + std::replace(path.begin(), path.end(), '\\', '/'); includeNewFile(path); } else { From 51b992617db6c5a55fa3288ab26673e2cfec2c2f Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 27 Mar 2020 21:25:09 +0100 Subject: [PATCH 05/13] Add +1 to error lines and put the file name in there --- src/sfizz/Synth.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index 1f0ab2eb..930e1c9e 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -85,12 +85,14 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vectorlexically_relative(parser.originalDirectory()); + std::cerr << "Parse error in " << relativePath << " at line " << range.start.lineNumber + 1 << ": " << message << '\n'; } void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& message) { - std::cerr << "Parse warning L" << range.start.lineNumber << ": " << message << '\n'; + const auto relativePath = range.start.filePath->lexically_relative(parser.originalDirectory()); + std::cerr << "Parse warning in " << relativePath << " at line " << range.start.lineNumber + 1 << ": " << message << '\n'; } void sfz::Synth::buildRegion(const std::vector& regionOpcodes) From 847af7b731dd0a846e7e3739466e246bce9fb9e4 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Fri, 27 Mar 2020 22:53:29 +0100 Subject: [PATCH 06/13] Added a way to parse from a string --- src/sfizz/parser/Parser.cpp | 22 +++++++++++++++++++++- src/sfizz/parser/Parser.h | 2 ++ src/sfizz/parser/ParserPrivate.cpp | 14 ++++++++++++++ src/sfizz/parser/ParserPrivate.h | 15 +++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 8007e40c..db286465 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -23,13 +23,18 @@ void Parser::addDefinition(absl::string_view id, absl::string_view value) _definitions[id] = std::string(value); } -void Parser::parseFile(const fs::path& path) +void Parser::reset() { _pathsIncluded.clear(); _currentHeader.reset(); _currentOpcodes.clear(); _errorCount = 0; _warningCount = 0; +} + +void Parser::parseFile(const fs::path& path) +{ + reset(); if (_listener) _listener->onParseBegin(); @@ -42,6 +47,21 @@ void Parser::parseFile(const fs::path& path) _listener->onParseEnd(); } +void Parser::parseString(absl::string_view sfzView) +{ + reset(); + + if (_listener) + _listener->onParseBegin(); + + _included.push_back(absl::make_unique(sfzView)); + processTopLevel(); + flushCurrentHeader(); + + if (_listener) + _listener->onParseEnd(); +} + void Parser::includeNewFile(const fs::path& path) { fs::path fullPath = diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h index cdcb50cd..17f88709 100644 --- a/src/sfizz/parser/Parser.h +++ b/src/sfizz/parser/Parser.h @@ -29,6 +29,7 @@ public: void addDefinition(absl::string_view id, absl::string_view value); void parseFile(const fs::path& path); + void parseString(absl::string_view sfzView); void setRecursiveIncludeGuardEnabled(bool en) { _recursiveIncludeGuardEnabled = en; } void setMaximumIncludeDepth(size_t depth) { _maxIncludeDepth = depth; } @@ -66,6 +67,7 @@ private: void processDirective(); void processHeader(); void processOpcode(); + void reset(); // errors and warnings void emitError(const SourceRange& range, const std::string& message); diff --git a/src/sfizz/parser/ParserPrivate.cpp b/src/sfizz/parser/ParserPrivate.cpp index 0db8423d..27d1d153 100644 --- a/src/sfizz/parser/ParserPrivate.cpp +++ b/src/sfizz/parser/ParserPrivate.cpp @@ -139,4 +139,18 @@ int FileReader::getNextStreamByte() return _fileStream.get(); } +StringViewReader::StringViewReader(absl::string_view sfzView) + : Reader({}), _sfzView(sfzView) +{ + +} + +int StringViewReader::getNextStreamByte() +{ + if (position < _sfzView.length()) + return _sfzView[position++]; + + return kEof; +} + } // namespace sfz diff --git a/src/sfizz/parser/ParserPrivate.h b/src/sfizz/parser/ParserPrivate.h index a0c2fecd..520f87a6 100644 --- a/src/sfizz/parser/ParserPrivate.h +++ b/src/sfizz/parser/ParserPrivate.h @@ -119,6 +119,21 @@ private: fs::ifstream _fileStream; }; +/** + * @brief String-view-based version of Reader. + */ +class StringViewReader : public Reader { +public: + explicit StringViewReader(absl::string_view sfzView); + +protected: + int getNextStreamByte() override; + +private: + absl::string_view _sfzView; + size_t position { 0 }; +}; + } // namespace sfz #include "ParserPrivate.hpp" From 5a292aca3110cf70d9545024d9a1917b5b556d55 Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 28 Mar 2020 00:26:37 +0100 Subject: [PATCH 07/13] End the value when hitting < too --- src/sfizz/parser/Parser.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index db286465..13572cea 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -256,20 +256,25 @@ void Parser::processOpcode() std::string valueRaw; extractToEol(reader, &valueRaw); - // if another "=" character was hit, it means we read too far - size_t position = valueRaw.find('='); + // if a "=" or "<" character was hit, it means we read too far + size_t position = valueRaw.find_first_of("=<"); if (position != valueRaw.npos) { - while (position > 0 && isRawOpcodeNameChar(valueRaw[position - 1])) - --position; - while (position > 0 && isSpaceChar(valueRaw[position - 1])) - --position; + char hitChar = valueRaw[position]; + + // if it was "=", rewind before the opcode name and spaces preceding + if (hitChar == '=') { + while (position > 0 && isRawOpcodeNameChar(valueRaw[position - 1])) + --position; + while (position > 0 && isSpaceChar(valueRaw[position - 1])) + --position; + } absl::string_view excess(&valueRaw[position], valueRaw.size() - position); reader.putBackChars(excess); valueRaw.resize(position); // ensure that we are landing back next to a space char - if (!reader.hasOneOfChars(" \t\r\n")) { + if (hitChar == '=' && !reader.hasOneOfChars(" \t\r\n")) { SourceLocation end = reader.location(); emitError({ valueStart, end }, "Unexpected `=` in opcode value."); recover(); From 42234da6500e251b7461604e1e8ea3e9706f5fdc Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 28 Mar 2020 00:26:50 +0100 Subject: [PATCH 08/13] Add tests for the new parser --- tests/ParsingT.cpp | 288 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) diff --git a/tests/ParsingT.cpp b/tests/ParsingT.cpp index cadcaa8b..968f1e57 100644 --- a/tests/ParsingT.cpp +++ b/tests/ParsingT.cpp @@ -5,8 +5,10 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #include "sfizz/SfzHelpers.h" +#include "sfizz/parser/Parser.h" #include #include "catch2/catch.hpp" +#include "absl/strings/string_view.h" using namespace Catch::literals; void includeTest(const std::string& line, const std::string& fileName) @@ -126,3 +128,289 @@ TEST_CASE("[Parsing] Member") memberTest("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav"); memberTest("sample=..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav", "sample", "..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav"); } + +// New parser + +struct ParsingMocker: sfz::Parser::Listener +{ + void onParseBegin() override + { + beginnings++; + } + void onParseEnd() override + { + endings++; + } + void onParseHeader(const sfz::SourceRange&, const std::string& header) override + { + headers.push_back(header); + } + void onParseOpcode(const sfz::SourceRange&, const sfz::SourceRange&, const std::string& name, const std::string& value) override + { + opcodes.emplace_back(name, value); + } + void onParseError(const sfz::SourceRange& range, const std::string&) override + { + errors.push_back(range); + } + void onParseWarning(const sfz::SourceRange& range, const std::string&) override + { + warnings.push_back(range); + } + // high-level parsing + void onParseFullBlock(const std::string& header, const std::vector& opcodes) override + { + fullBlockHeaders.push_back(header); + fullBlockMembers.push_back(opcodes); + } + + int beginnings { 0 }; + int endings { 0 }; + std::vector errors; + std::vector warnings; + std::vector opcodes; + std::vector headers; + std::vector fullBlockHeaders; + std::vector> fullBlockMembers; +}; + +TEST_CASE("[Parsing] Empty") +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString(""); + REQUIRE(mock.beginnings == 1); + REQUIRE(mock.endings == 1); + REQUIRE(mock.errors.empty()); + REQUIRE(mock.warnings.empty()); + REQUIRE(mock.opcodes.empty()); + REQUIRE(mock.headers.empty()); + REQUIRE(mock.fullBlockHeaders.empty()); + REQUIRE(mock.fullBlockMembers.empty()); +} + +static const absl::string_view emptySfz { +R"( + +)" +}; + +TEST_CASE("[Parsing] Empty2") +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString(emptySfz); + REQUIRE(mock.beginnings == 1); + REQUIRE(mock.endings == 1); + REQUIRE(mock.errors.empty()); + REQUIRE(mock.warnings.empty()); + REQUIRE(mock.opcodes.empty()); + REQUIRE(mock.headers.empty()); + REQUIRE(mock.fullBlockHeaders.empty()); + REQUIRE(mock.fullBlockMembers.empty()); +} + +namespace sfz{ +bool operator==(const Opcode& lhs, const Opcode& rhs) +{ + return (lhs.opcode == rhs.opcode) && (lhs.value == rhs.value); +} +} + +TEST_CASE("[Parsing] Jpcima good region") +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString(R"( + sample=*silence key=69 +sample=My Directory/My Wave.wav // path with spaces and a comment +sample=My Directory/My Wave.wav key=69 // path with spaces, and other opcode following +)"); + std::vector> expectedMembers = { + {{"sample", "*silence"}, {"key", "69"}, {"sample", "My Directory/My Wave.wav"}, {"sample", "My Directory/My Wave.wav"}, {"key", "69"}} + }; + std::vector expectedHeaders = { + "region" + }; + std::vector expectedOpcodes; + + for (auto& members: expectedMembers) + for (auto& opcode: members) + expectedOpcodes.push_back(opcode); + + REQUIRE(mock.beginnings == 1); + REQUIRE(mock.endings == 1); + REQUIRE(mock.errors.empty()); + REQUIRE(mock.warnings.empty()); + REQUIRE(mock.opcodes == expectedOpcodes); + REQUIRE(mock.headers == expectedHeaders); + REQUIRE(mock.fullBlockHeaders == expectedHeaders); + REQUIRE(mock.fullBlockMembers == expectedMembers); +} + +void memberTestNew(absl::string_view member, absl::string_view opcode, absl::string_view value) +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString(absl::StrCat(" ", member)); + REQUIRE(mock.opcodes.size() == 1); + REQUIRE(mock.headers.size() == 1); + REQUIRE(mock.fullBlockHeaders.size() == 1); + REQUIRE(mock.fullBlockMembers.size() == 1); + REQUIRE(mock.headers[0] == "region"); + REQUIRE(mock.opcodes[0].opcode == opcode); + REQUIRE(mock.opcodes[0].value == value); + REQUIRE(mock.fullBlockHeaders[0] == "region"); + REQUIRE(mock.fullBlockMembers[0][0].opcode == opcode); + REQUIRE(mock.fullBlockMembers[0][0].value == value); +} + +TEST_CASE("[Parsing] Members (new parser)") +{ + memberTestNew("param=value", "param", "value"); + memberTestNew("param=113", "param", "113"); + memberTestNew("param1=value", "param1", "value"); + memberTestNew("param_1=value", "param_1", "value"); + memberTestNew("param_1=value", "param_1", "value"); + memberTestNew("ampeg_sustain_oncc74=-100", "ampeg_sustain_oncc74", "-100"); + memberTestNew("lorand=0.750", "lorand", "0.750"); + memberTestNew("sample=value", "sample", "value"); + memberTestNew("sample=value-()*", "sample", "value-()*"); + memberTestNew("sample=../sample.wav", "sample", "../sample.wav"); + memberTestNew("sample=..\\sample.wav", "sample", "..\\sample.wav"); + memberTestNew("sample=subdir\\subdir\\sample.wav", "sample", "subdir\\subdir\\sample.wav"); + memberTestNew("sample=subdir/subdir/sample.wav", "sample", "subdir/subdir/sample.wav"); + memberTestNew("sample=subdir_underscore\\sample.wav", "sample", "subdir_underscore\\sample.wav"); + memberTestNew("sample=subdir space\\sample.wav", "sample", "subdir space\\sample.wav"); + memberTestNew("sample=..\\Samples\\pizz\\a0_vl3_rr3.wav", "sample", "..\\Samples\\pizz\\a0_vl3_rr3.wav"); + memberTestNew("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav"); + memberTestNew("sample=..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav", "sample", "..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav"); +} + +TEST_CASE("[Parsing] bad headers") +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString( +R"(<> + dummy_member=no +)" + ); + REQUIRE(mock.errors.size() == 2); + REQUIRE(mock.errors[0].start.lineNumber == 0); + REQUIRE(mock.errors[0].start.columnNumber == 0); + REQUIRE(mock.errors[1].start.lineNumber == 1); + REQUIRE(mock.errors[1].start.columnNumber == 0); + REQUIRE(mock.errors[0].end.lineNumber == 0); + REQUIRE(mock.errors[0].end.columnNumber == 2); + REQUIRE(mock.errors[1].end.lineNumber == 1); + REQUIRE(mock.errors[1].end.columnNumber == 7); +} + +void defineTestNew(const std::string& directive, const std::string& variable, const std::string& value) +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString(directive); + const auto defines = parser.getDefines(); + REQUIRE(defines.contains(variable)); + REQUIRE(defines.at(variable) == value); +} + +TEST_CASE("[Parsing] #define (new parser)") +{ + defineTestNew("#define $empty ", "empty", ""); + defineTestNew("#define $number 1", "number", "1"); + defineTestNew("#define $letters QWERasdf", "letters", "QWERasdf"); + defineTestNew("#define $alphanum asr1t44", "alphanum", "asr1t44"); + defineTestNew("#define $whitespace asr1t44 ", "whitespace", "asr1t44"); + // The new parser does greedy matching + // defineTestNew("#define $lazyMatching matched bfasd ", "lazyMatching", "matched"); + defineTestNew("#define $stircut -12", "stircut", "-12"); + defineTestNew("#define $_ht_under_score_ 3fd", "_ht_under_score_", "3fd"); + defineTestNew("#define $ht_under_score 3fd", "ht_under_score", "3fd"); +} + +TEST_CASE("[Parsing] Malformed includes") +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString(R"(#include "MyFileWhichDoesNotExist1.sfz +#include MyFileWhichDoesNotExist1.sfz)"); + REQUIRE(mock.errors.size() == 2); + REQUIRE(mock.errors[0].start.lineNumber == 0); + REQUIRE(mock.errors[0].start.columnNumber == 0); + REQUIRE(mock.errors[1].start.lineNumber == 1); + REQUIRE(mock.errors[1].start.columnNumber == 0); + REQUIRE(mock.errors[0].end.lineNumber == 0); + REQUIRE(mock.errors[0].end.columnNumber == 38); + REQUIRE(mock.errors[1].end.lineNumber == 1); + REQUIRE(mock.errors[1].end.columnNumber == 9); +} + +TEST_CASE("[Parsing] Headers (new parser)") +{ + SECTION("Basic header match") + { + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString("
param1=value1 param2=value2 "); + std::vector> expectedMembers = { + {{"param1", "value1"}, {"param2", "value2"}}, + {} + }; + std::vector expectedHeaders = { + "header", "next" + }; + std::vector expectedOpcodes; + + for (auto& members: expectedMembers) + for (auto& opcode: members) + expectedOpcodes.push_back(opcode); + + REQUIRE(mock.beginnings == 1); + REQUIRE(mock.endings == 1); + REQUIRE(mock.errors.empty()); + REQUIRE(mock.warnings.empty()); + REQUIRE(mock.opcodes == expectedOpcodes); + REQUIRE(mock.headers == expectedHeaders); + REQUIRE(mock.fullBlockHeaders == expectedHeaders); + REQUIRE(mock.fullBlockMembers == expectedMembers); + } + + SECTION("EOL header match") + { + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.parseString("
param1=value1 param2=value2"); + std::vector> expectedMembers = { + {{"param1", "value1"}, {"param2", "value2"}} + }; + std::vector expectedHeaders = { + "header" + }; + std::vector expectedOpcodes; + + for (auto& members: expectedMembers) + for (auto& opcode: members) + expectedOpcodes.push_back(opcode); + + REQUIRE(mock.beginnings == 1); + REQUIRE(mock.endings == 1); + REQUIRE(mock.errors.empty()); + REQUIRE(mock.warnings.empty()); + REQUIRE(mock.opcodes == expectedOpcodes); + REQUIRE(mock.headers == expectedHeaders); + REQUIRE(mock.fullBlockHeaders == expectedHeaders); + REQUIRE(mock.fullBlockMembers == expectedMembers); + } +} From 25fa15acbeedff9e429b50bd48bb7952de33569d Mon Sep 17 00:00:00 2001 From: Paul Ferrand Date: Sat, 28 Mar 2020 00:45:59 +0100 Subject: [PATCH 09/13] Add an originalDirectory setter and change the logic for parseFile --- src/sfizz/parser/Parser.cpp | 17 ++++++++++++++--- src/sfizz/parser/Parser.h | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 13572cea..f54398ac 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -39,6 +39,14 @@ void Parser::parseFile(const fs::path& path) if (_listener) _listener->onParseBegin(); + if (path.empty()) + return; + + if (path.is_relative()) + setOriginalDirectory(originalDirectory() / path); + else + setOriginalDirectory(path.parent_path()); + includeNewFile(path); processTopLevel(); flushCurrentHeader(); @@ -62,14 +70,17 @@ void Parser::parseString(absl::string_view sfzView) _listener->onParseEnd(); } +void Parser::setOriginalDirectory(const fs::path& originalDirectory) noexcept +{ + _originalDirectory = originalDirectory; +} + void Parser::includeNewFile(const fs::path& path) { fs::path fullPath = (path.empty() || path.is_absolute()) ? path : _originalDirectory / path; - if (_pathsIncluded.empty()) - _originalDirectory = fullPath.parent_path(); - else if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) { + if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) { if (_recursiveIncludeGuardEnabled) return; } diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h index 17f88709..5ab1d75b 100644 --- a/src/sfizz/parser/Parser.h +++ b/src/sfizz/parser/Parser.h @@ -35,6 +35,7 @@ public: void setMaximumIncludeDepth(size_t depth) { _maxIncludeDepth = depth; } const fs::path& originalDirectory() const noexcept { return _originalDirectory; } + void setOriginalDirectory(const fs::path& originalDirectory) noexcept; typedef absl::flat_hash_set IncludeFileSet; typedef absl::flat_hash_map DefinitionSet; From ba23ed810afd20521da875ffebb85b2f284e9daf Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 28 Mar 2020 01:45:59 +0100 Subject: [PATCH 10/13] String parsing without originalDirectory setter --- src/sfizz/parser/Parser.cpp | 61 ++++++++++++------------------ src/sfizz/parser/Parser.h | 6 +-- src/sfizz/parser/ParserPrivate.cpp | 5 +-- src/sfizz/parser/ParserPrivate.h | 2 +- tests/DemoParser.cpp | 9 +---- tests/ParsingT.cpp | 19 +++++----- 6 files changed, 41 insertions(+), 61 deletions(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index f54398ac..0442081a 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -33,21 +33,23 @@ void Parser::reset() } void Parser::parseFile(const fs::path& path) +{ + parseVirtualFile(path, nullptr); +} + +void Parser::parseString(const fs::path& path, absl::string_view sfzView) +{ + parseVirtualFile(path, absl::make_unique(path, sfzView)); +} + +void Parser::parseVirtualFile(const fs::path& path, std::unique_ptr reader) { reset(); if (_listener) _listener->onParseBegin(); - if (path.empty()) - return; - - if (path.is_relative()) - setOriginalDirectory(originalDirectory() / path); - else - setOriginalDirectory(path.parent_path()); - - includeNewFile(path); + includeNewFile(path, std::move(reader)); processTopLevel(); flushCurrentHeader(); @@ -55,32 +57,14 @@ void Parser::parseFile(const fs::path& path) _listener->onParseEnd(); } -void Parser::parseString(absl::string_view sfzView) -{ - reset(); - - if (_listener) - _listener->onParseBegin(); - - _included.push_back(absl::make_unique(sfzView)); - processTopLevel(); - flushCurrentHeader(); - - if (_listener) - _listener->onParseEnd(); -} - -void Parser::setOriginalDirectory(const fs::path& originalDirectory) noexcept -{ - _originalDirectory = originalDirectory; -} - -void Parser::includeNewFile(const fs::path& path) +void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader) { fs::path fullPath = (path.empty() || path.is_absolute()) ? path : _originalDirectory / path; - if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) { + if (_pathsIncluded.empty()) + _originalDirectory = fullPath.parent_path(); + else if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) { if (_recursiveIncludeGuardEnabled) return; } @@ -92,15 +76,18 @@ void Parser::includeNewFile(const fs::path& path) return; } - auto fileReader = absl::make_unique(fullPath); - if (fileReader->hasError()) { - SourceLocation loc = fileReader->location(); - emitError({ loc, loc }, "Cannot open file for reading: " + fullPath.string()); - return; + if (!reader) { + auto fileReader = absl::make_unique(fullPath); + if (fileReader->hasError()) { + SourceLocation loc = fileReader->location(); + emitError({ loc, loc }, "Cannot open file for reading: " + fullPath.string()); + return; + } + reader = std::move(fileReader); } _pathsIncluded.insert(fullPath.string()); - _included.push_back(std::move(fileReader)); + _included.push_back(std::move(reader)); } void Parser::processTopLevel() diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h index 5ab1d75b..56b3d7ca 100644 --- a/src/sfizz/parser/Parser.h +++ b/src/sfizz/parser/Parser.h @@ -29,13 +29,13 @@ public: void addDefinition(absl::string_view id, absl::string_view value); void parseFile(const fs::path& path); - void parseString(absl::string_view sfzView); + void parseString(const fs::path& path, absl::string_view sfzView); + void parseVirtualFile(const fs::path& path, std::unique_ptr reader); void setRecursiveIncludeGuardEnabled(bool en) { _recursiveIncludeGuardEnabled = en; } void setMaximumIncludeDepth(size_t depth) { _maxIncludeDepth = depth; } const fs::path& originalDirectory() const noexcept { return _originalDirectory; } - void setOriginalDirectory(const fs::path& originalDirectory) noexcept; typedef absl::flat_hash_set IncludeFileSet; typedef absl::flat_hash_map DefinitionSet; @@ -63,7 +63,7 @@ public: void setListener(Listener* listener) noexcept { _listener = listener; } private: - void includeNewFile(const fs::path& path); + void includeNewFile(const fs::path& path, std::unique_ptr reader = nullptr); void processTopLevel(); void processDirective(); void processHeader(); diff --git a/src/sfizz/parser/ParserPrivate.cpp b/src/sfizz/parser/ParserPrivate.cpp index 27d1d153..38a99059 100644 --- a/src/sfizz/parser/ParserPrivate.cpp +++ b/src/sfizz/parser/ParserPrivate.cpp @@ -139,10 +139,9 @@ int FileReader::getNextStreamByte() return _fileStream.get(); } -StringViewReader::StringViewReader(absl::string_view sfzView) - : Reader({}), _sfzView(sfzView) +StringViewReader::StringViewReader(const fs::path& filePath, absl::string_view sfzView) + : Reader(filePath), _sfzView(sfzView) { - } int StringViewReader::getNextStreamByte() diff --git a/src/sfizz/parser/ParserPrivate.h b/src/sfizz/parser/ParserPrivate.h index 520f87a6..7e271e26 100644 --- a/src/sfizz/parser/ParserPrivate.h +++ b/src/sfizz/parser/ParserPrivate.h @@ -124,7 +124,7 @@ private: */ class StringViewReader : public Reader { public: - explicit StringViewReader(absl::string_view sfzView); + explicit StringViewReader(const fs::path& filePath, absl::string_view sfzView); protected: int getNextStreamByte() override; diff --git a/tests/DemoParser.cpp b/tests/DemoParser.cpp index 42893a76..278cb353 100644 --- a/tests/DemoParser.cpp +++ b/tests/DemoParser.cpp @@ -2,7 +2,6 @@ #include "parser/Parser.h" #include #include -#include #include #include #include @@ -114,14 +113,8 @@ void Application::requestParseCheck() void Application::runParseCheck() { QByteArray code = _ui.sfzEdit->toPlainText().toLatin1(); - - QTemporaryFile temp(QDir::tempPath() + "/parseXXXXXX.sfz"); - temp.open(); - temp.write(code); - temp.flush(); - _blockTextChanged = true; - _parser.parseFile(temp.fileName().toStdString()); + _parser.parseString("/virtual.sfz", absl::string_view(code.data(), code.size())); _blockTextChanged = false; } diff --git a/tests/ParsingT.cpp b/tests/ParsingT.cpp index 968f1e57..5d35a1a9 100644 --- a/tests/ParsingT.cpp +++ b/tests/ParsingT.cpp @@ -179,7 +179,7 @@ TEST_CASE("[Parsing] Empty") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString(""); + parser.parseString("/empty.sfz", ""); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); @@ -201,7 +201,7 @@ TEST_CASE("[Parsing] Empty2") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString(emptySfz); + parser.parseString("/empty2.sfz", emptySfz); REQUIRE(mock.beginnings == 1); REQUIRE(mock.endings == 1); REQUIRE(mock.errors.empty()); @@ -224,7 +224,7 @@ TEST_CASE("[Parsing] Jpcima good region") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString(R"( + parser.parseString("/goodRegion.sfz", R"( sample=*silence key=69 sample=My Directory/My Wave.wav // path with spaces and a comment sample=My Directory/My Wave.wav key=69 // path with spaces, and other opcode following @@ -256,7 +256,7 @@ void memberTestNew(absl::string_view member, absl::string_view opcode, absl::str sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString(absl::StrCat(" ", member)); + parser.parseString("/memberTestNew.sfz", absl::StrCat(" ", member)); REQUIRE(mock.opcodes.size() == 1); REQUIRE(mock.headers.size() == 1); REQUIRE(mock.fullBlockHeaders.size() == 1); @@ -296,7 +296,7 @@ TEST_CASE("[Parsing] bad headers") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString( + parser.parseString("/badHeaders.sfz", R"(<> dummy_member=no )" @@ -317,7 +317,7 @@ void defineTestNew(const std::string& directive, const std::string& variable, co sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString(directive); + parser.parseString("/defineTestNew.sfz", directive); const auto defines = parser.getDefines(); REQUIRE(defines.contains(variable)); REQUIRE(defines.at(variable) == value); @@ -342,7 +342,8 @@ TEST_CASE("[Parsing] Malformed includes") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString(R"(#include "MyFileWhichDoesNotExist1.sfz + parser.parseString("/malformedIncludes.sfz", +R"(#include "MyFileWhichDoesNotExist1.sfz #include MyFileWhichDoesNotExist1.sfz)"); REQUIRE(mock.errors.size() == 2); REQUIRE(mock.errors[0].start.lineNumber == 0); @@ -362,7 +363,7 @@ TEST_CASE("[Parsing] Headers (new parser)") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString("
param1=value1 param2=value2 "); + parser.parseString("/headers.sfz", "
param1=value1 param2=value2 "); std::vector> expectedMembers = { {{"param1", "value1"}, {"param2", "value2"}}, {} @@ -391,7 +392,7 @@ TEST_CASE("[Parsing] Headers (new parser)") sfz::Parser parser; ParsingMocker mock; parser.setListener(&mock); - parser.parseString("
param1=value1 param2=value2"); + parser.parseString("/eolHeaderMatch.sfz", "
param1=value1 param2=value2"); std::vector> expectedMembers = { {{"param1", "value1"}, {"param2", "value2"}} }; From 3871d9c9b29ac79ec5d9f046be08166313cc1bb8 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 28 Mar 2020 02:11:20 +0100 Subject: [PATCH 11/13] Let the parser identify the origin of a failed #include --- src/sfizz/parser/Parser.cpp | 24 ++++++++++++++++-------- src/sfizz/parser/Parser.h | 4 +++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 0442081a..5994f51f 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -49,7 +49,7 @@ void Parser::parseVirtualFile(const fs::path& path, std::unique_ptr read if (_listener) _listener->onParseBegin(); - includeNewFile(path, std::move(reader)); + includeNewFile(path, std::move(reader), {}); processTopLevel(); flushCurrentHeader(); @@ -57,7 +57,7 @@ void Parser::parseVirtualFile(const fs::path& path, std::unique_ptr read _listener->onParseEnd(); } -void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader) +void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader, const SourceRange& includeStmtRange) { fs::path fullPath = (path.empty() || path.is_absolute()) ? path : _originalDirectory / path; @@ -69,10 +69,17 @@ void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader return; } + auto makeErrorRange = [&]() -> SourceRange { + if (!includeStmtRange) { + SourceLocation loc; + loc.filePath = std::make_shared(fullPath); + return {loc, loc}; + } + return includeStmtRange; + }; + if (_included.size() == _maxIncludeDepth) { - SourceLocation loc; - loc.filePath = std::make_shared(fullPath); - emitError({ loc, loc }, "Exceeded maximum include depth (" + std::to_string(_maxIncludeDepth) + ")"); + emitError(makeErrorRange(), "Exceeded maximum include depth (" + std::to_string(_maxIncludeDepth) + ")"); return; } @@ -80,7 +87,7 @@ void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader auto fileReader = absl::make_unique(fullPath); if (fileReader->hasError()) { SourceLocation loc = fileReader->location(); - emitError({ loc, loc }, "Cannot open file for reading: " + fullPath.string()); + emitError(makeErrorRange(), "Cannot open file for reading: " + fullPath.string()); return; } reader = std::move(fileReader); @@ -159,15 +166,16 @@ void Parser::processDirective() valid = reader.extractExactChar('"'); } + SourceLocation end = reader.location(); + if (!valid) { - SourceLocation end = reader.location(); emitError({ start, end }, "Expected \"file.sfz\" after #include."); recover(); return; } std::replace(path.begin(), path.end(), '\\', '/'); - includeNewFile(path); + includeNewFile(path, nullptr, { start, end }); } else { SourceLocation end = reader.location(); diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h index 56b3d7ca..4e89b402 100644 --- a/src/sfizz/parser/Parser.h +++ b/src/sfizz/parser/Parser.h @@ -63,7 +63,7 @@ public: void setListener(Listener* listener) noexcept { _listener = listener; } private: - void includeNewFile(const fs::path& path, std::unique_ptr reader = nullptr); + void includeNewFile(const fs::path& path, std::unique_ptr reader, const SourceRange& includeStmtRange); void processTopLevel(); void processDirective(); void processHeader(); @@ -122,6 +122,7 @@ struct SourceLocation { std::shared_ptr filePath; size_t lineNumber = 0; size_t columnNumber = 0; + explicit operator bool() const noexcept { return filePath != nullptr; } }; /** @@ -130,6 +131,7 @@ struct SourceLocation { struct SourceRange { SourceLocation start; SourceLocation end; + explicit operator bool() const noexcept { return bool(start) && bool(end); } }; } // namespace sfz From 6ee9dd106eb168a91111e5b0dd247b5d14eb86f1 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 28 Mar 2020 02:44:06 +0100 Subject: [PATCH 12/13] Add proper support of external #define --- src/sfizz/parser/Parser.cpp | 25 ++++++++++++++++++------- src/sfizz/parser/Parser.h | 10 +++++++--- tests/ParsingT.cpp | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/sfizz/parser/Parser.cpp b/src/sfizz/parser/Parser.cpp index 5994f51f..eb3c496d 100644 --- a/src/sfizz/parser/Parser.cpp +++ b/src/sfizz/parser/Parser.cpp @@ -18,20 +18,26 @@ Parser::~Parser() { } -void Parser::addDefinition(absl::string_view id, absl::string_view value) -{ - _definitions[id] = std::string(value); -} - void Parser::reset() { _pathsIncluded.clear(); + _currentDefinitions = _externalDefinitions; _currentHeader.reset(); _currentOpcodes.clear(); _errorCount = 0; _warningCount = 0; } +void Parser::addExternalDefinition(absl::string_view id, absl::string_view value) +{ + _externalDefinitions[id] = std::string(value); +} + +void Parser::clearExternalDefinitions() +{ + _externalDefinitions.clear(); +} + void Parser::parseFile(const fs::path& path) { parseVirtualFile(path, nullptr); @@ -97,6 +103,11 @@ void Parser::includeNewFile(const fs::path& path, std::unique_ptr reader _included.push_back(std::move(reader)); } +void Parser::addDefinition(absl::string_view id, absl::string_view value) +{ + _currentDefinitions[id] = std::string(value); +} + void Parser::processTopLevel() { while (!_included.empty()) { @@ -404,8 +415,8 @@ std::string Parser::expandDollarVars(const SourceRange& range, absl::string_view continue; } - auto it = _definitions.find(name); - if (it == _definitions.end()) { + auto it = _currentDefinitions.find(name); + if (it == _currentDefinitions.end()) { emitWarning(range, "The variable `" + name + "` is not defined."); continue; } diff --git a/src/sfizz/parser/Parser.h b/src/sfizz/parser/Parser.h index 4e89b402..4f6a071d 100644 --- a/src/sfizz/parser/Parser.h +++ b/src/sfizz/parser/Parser.h @@ -27,7 +27,9 @@ public: Parser(); ~Parser(); - void addDefinition(absl::string_view id, absl::string_view value); + void addExternalDefinition(absl::string_view id, absl::string_view value); + void clearExternalDefinitions(); + void parseFile(const fs::path& path); void parseString(const fs::path& path, absl::string_view sfzView); void parseVirtualFile(const fs::path& path, std::unique_ptr reader); @@ -41,7 +43,7 @@ public: typedef absl::flat_hash_map DefinitionSet; const IncludeFileSet& getIncludedFiles() const noexcept { return _pathsIncluded; } - const DefinitionSet& getDefines() const noexcept { return _definitions; } + const DefinitionSet& getDefines() const noexcept { return _currentDefinitions; } size_t getErrorCount() const noexcept { return _errorCount; } size_t getWarningCount() const noexcept { return _warningCount; } @@ -64,6 +66,7 @@ public: private: void includeNewFile(const fs::path& path, std::unique_ptr reader, const SourceRange& includeStmtRange); + void addDefinition(absl::string_view id, absl::string_view value); void processTopLevel(); void processDirective(); void processHeader(); @@ -96,7 +99,7 @@ private: Listener* _listener = nullptr; fs::path _originalDirectory { fs::current_path() }; - DefinitionSet _definitions; + DefinitionSet _externalDefinitions; // a current list of files included, last one at the back std::vector> _included; @@ -105,6 +108,7 @@ private: size_t _maxIncludeDepth = 32; bool _recursiveIncludeGuardEnabled = false; IncludeFileSet _pathsIncluded; + DefinitionSet _currentDefinitions; // parsing state absl::optional _currentHeader; diff --git a/tests/ParsingT.cpp b/tests/ParsingT.cpp index 5d35a1a9..7a51263c 100644 --- a/tests/ParsingT.cpp +++ b/tests/ParsingT.cpp @@ -415,3 +415,36 @@ TEST_CASE("[Parsing] Headers (new parser)") REQUIRE(mock.fullBlockMembers == expectedMembers); } } + +TEST_CASE("[Parsing] External definitions") +{ + sfz::Parser parser; + ParsingMocker mock; + parser.setListener(&mock); + parser.addExternalDefinition("foo", "abc"); + parser.addExternalDefinition("bar", "123"); + parser.parseString("/externalDefinitions.sfz", +R"(
+param1=$foo +param2=$bar)"); + std::vector> expectedMembers = { + {{"param1", "abc"}, {"param2", "123"}} + }; + std::vector expectedHeaders = { + "header" + }; + std::vector expectedOpcodes; + + for (auto& members: expectedMembers) + for (auto& opcode: members) + expectedOpcodes.push_back(opcode); + + REQUIRE(mock.beginnings == 1); + REQUIRE(mock.endings == 1); + REQUIRE(mock.errors.empty()); + REQUIRE(mock.warnings.empty()); + REQUIRE(mock.opcodes == expectedOpcodes); + REQUIRE(mock.headers == expectedHeaders); + REQUIRE(mock.fullBlockHeaders == expectedHeaders); + REQUIRE(mock.fullBlockMembers == expectedMembers); +} From a15a50b96ca057a42b8b4d23163084f07a79d14a Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Sat, 28 Mar 2020 02:53:50 +0100 Subject: [PATCH 13/13] Make the parser demo UI a bit more practical --- tests/DemoParser.cpp | 3 +++ tests/DemoParser.ui | 13 +------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/tests/DemoParser.cpp b/tests/DemoParser.cpp index 278cb353..f7931694 100644 --- a/tests/DemoParser.cpp +++ b/tests/DemoParser.cpp @@ -99,6 +99,9 @@ void Application::init() _ui.messageTable->setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Line") << tr("Message")); _ui.messageTable->horizontalHeader()->setStretchLastSection(true); + _ui.splitter->setStretchFactor(0, 3); + _ui.splitter->setStretchFactor(1, 1); + window->show(); _parser.setListener(this); diff --git a/tests/DemoParser.ui b/tests/DemoParser.ui index 6198fd1a..01404639 100644 --- a/tests/DemoParser.ui +++ b/tests/DemoParser.ui @@ -7,7 +7,7 @@ 0 0 800 - 600 + 800 @@ -35,17 +35,6 @@ - - - - 0 - 0 - 800 - 20 - - - -