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)