Initial parser rework
This commit is contained in:
parent
fa150e2668
commit
30c8d25a59
14 changed files with 1100 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<std::string>& lines) noexcept
|
||||
void sfz::OldParser::readSfzFile(const fs::path& fileName, std::vector<std::string>& lines) noexcept
|
||||
{
|
||||
std::ifstream fileStream(fileName.c_str());
|
||||
if (!fileStream)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@
|
|||
#include <vector>
|
||||
|
||||
namespace sfz {
|
||||
class Parser {
|
||||
class OldParser {
|
||||
public:
|
||||
virtual ~Parser() = default;
|
||||
virtual ~OldParser() = default;
|
||||
virtual bool loadSfzFile(const fs::path& file);
|
||||
const std::map<std::string, std::string>& getDefines() const noexcept { return defines; }
|
||||
const std::vector<fs::path>& getIncludedFiles() const noexcept { return includedFiles; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
383
src/sfizz/parser/Parser.cpp
Normal file
383
src/sfizz/parser/Parser.cpp
Normal file
|
|
@ -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<FileReader>(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
|
||||
109
src/sfizz/parser/Parser.h
Normal file
109
src/sfizz/parser/Parser.h
Normal file
|
|
@ -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 <string>
|
||||
#include <memory>
|
||||
|
||||
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<std::string, std::string> _definitions;
|
||||
|
||||
// a current list of files included, last one at the back
|
||||
std::vector<std::unique_ptr<Reader>> _included;
|
||||
|
||||
// recursive include guard
|
||||
absl::flat_hash_set<std::string> _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<fs::path> filePath;
|
||||
size_t lineNumber = 0;
|
||||
size_t columnNumber = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Range of source file.
|
||||
*/
|
||||
struct SourceRange {
|
||||
SourceLocation start;
|
||||
SourceLocation end;
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
142
src/sfizz/parser/ParserPrivate.cpp
Normal file
142
src/sfizz/parser/ParserPrivate.cpp
Normal file
|
|
@ -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<fs::path>(filePath);
|
||||
_lineNumColumns.reserve(256);
|
||||
}
|
||||
|
||||
int Reader::getChar()
|
||||
{
|
||||
int byte;
|
||||
|
||||
if (_accum.empty())
|
||||
byte = getNextStreamByte();
|
||||
else {
|
||||
byte = static_cast<unsigned char>(_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<unsigned char>(_accum.back());
|
||||
|
||||
return byte;
|
||||
}
|
||||
|
||||
bool Reader::extractExactChar(char c)
|
||||
{
|
||||
int next = peekChar();
|
||||
|
||||
if (next == kEof || next != static_cast<unsigned char>(c))
|
||||
return false;
|
||||
|
||||
getChar();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Reader::putBackChar(int c)
|
||||
{
|
||||
if (c == kEof)
|
||||
return;
|
||||
|
||||
char c8bit = static_cast<unsigned char>(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<unsigned char>(characters[i]));
|
||||
}
|
||||
|
||||
size_t Reader::skipChars(absl::string_view chars)
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
while (chars.find(static_cast<unsigned char>(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<unsigned char>(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
|
||||
123
src/sfizz/parser/ParserPrivate.h
Normal file
123
src/sfizz/parser/ParserPrivate.h
Normal file
|
|
@ -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 <string>
|
||||
|
||||
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<char>::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 <class P> size_t extractWhile(std::string* dst, const P& pred);
|
||||
|
||||
/**
|
||||
* @brief Extract until as a predicate does not hold on the next character.
|
||||
*/
|
||||
template <class P> 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 <class P> size_t skipWhile(const P& pred);
|
||||
|
||||
/**
|
||||
* @brief Skip until as a predicate does not hold on the next character.
|
||||
*/
|
||||
template <class P> 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<int> _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"
|
||||
50
src/sfizz/parser/ParserPrivate.hpp
Normal file
50
src/sfizz/parser/ParserPrivate.hpp
Normal file
|
|
@ -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 <class P>
|
||||
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<unsigned char>(byte));
|
||||
if (!more)
|
||||
putBackChar(byte);
|
||||
else {
|
||||
if (dst)
|
||||
dst->push_back(static_cast<unsigned char>(byte));
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
size_t Reader::extractUntil(std::string* dst, const P& pred)
|
||||
{
|
||||
return extractWhile(dst, [&pred](char c) -> bool { return !pred(c); });
|
||||
}
|
||||
|
||||
template <class P>
|
||||
size_t Reader::skipWhile(const P& pred)
|
||||
{
|
||||
return extractWhile(nullptr, pred);
|
||||
}
|
||||
|
||||
template <class P>
|
||||
size_t Reader::skipUntil(const P& pred)
|
||||
{
|
||||
return extractUntil(nullptr, pred);
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
228
tests/DemoParser.cpp
Normal file
228
tests/DemoParser.cpp
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#include "ui_DemoParser.h"
|
||||
#include "sfizz/parser/Parser.h"
|
||||
#include <QApplication>
|
||||
#include <QTimer>
|
||||
#include <QTemporaryFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QTextBlock>
|
||||
#include <QDebug>
|
||||
|
||||
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
|
||||
<ab@cd> // bad identifier
|
||||
|
||||
<region>
|
||||
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();
|
||||
}
|
||||
52
tests/DemoParser.ui
Normal file
52
tests/DemoParser.ui
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Sfizz parser demo</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QTextEdit" name="sfzEdit"/>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Messages</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="messageTable"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>20</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue