Merge pull request #230 from jpcima/parser-definitions
parser: dollar expansions and multiple #define on the same line
This commit is contained in:
commit
30d5d7a6b5
3 changed files with 288 additions and 46 deletions
|
|
@ -163,7 +163,18 @@ void Parser::processDirective()
|
|||
|
||||
std::string value;
|
||||
extractToEol(reader, &value);
|
||||
|
||||
#if 1
|
||||
// ARIA/not Cakewalk: cut the value after the first word
|
||||
size_t position = value.find_first_of(" \t");
|
||||
if (position != value.npos) {
|
||||
absl::string_view excess(&value[position], value.size() - position);
|
||||
reader.putBackChars(excess);
|
||||
value.resize(position);
|
||||
}
|
||||
#else
|
||||
trimRight(value);
|
||||
#endif
|
||||
|
||||
addDefinition(id, value);
|
||||
}
|
||||
|
|
@ -274,36 +285,47 @@ void Parser::processOpcode()
|
|||
std::string valueRaw;
|
||||
extractToEol(reader, &valueRaw);
|
||||
|
||||
// if a "=" or "<" character was hit, it means we read too far
|
||||
size_t position = valueRaw.find_first_of("=<");
|
||||
if (position != valueRaw.npos) {
|
||||
char hitChar = valueRaw[position];
|
||||
size_t endPosition = 0;
|
||||
|
||||
// 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;
|
||||
for (size_t valueSize = valueRaw.size(); endPosition < valueSize;) {
|
||||
size_t i = endPosition + 1;
|
||||
|
||||
if (isSpaceChar(valueRaw[endPosition])) {
|
||||
// check if the rest of the string is to consume or not
|
||||
bool stop = false;
|
||||
|
||||
// consume space characters following
|
||||
while (i < valueSize && isSpaceChar(valueRaw[i]))
|
||||
++i;
|
||||
|
||||
// if there aren't non-space characters following, do not extract
|
||||
if (i == valueSize)
|
||||
stop = true;
|
||||
// if a "=" or "<" character is next, a header or a directive follows
|
||||
else if (valueRaw[i] == '<' || valueRaw[i] == '#')
|
||||
stop = true;
|
||||
// if sequence of identifier chars and then "=", an opcode follows
|
||||
else if (isIdentifierChar(valueRaw[i])) {
|
||||
++i;
|
||||
while (i < valueSize && isIdentifierChar(valueRaw[i]))
|
||||
++i;
|
||||
if (i < valueSize && valueRaw[i] == '=')
|
||||
stop = true;
|
||||
}
|
||||
|
||||
if (stop)
|
||||
break;
|
||||
}
|
||||
|
||||
absl::string_view excess(&valueRaw[position], valueRaw.size() - position);
|
||||
endPosition = i;
|
||||
}
|
||||
|
||||
if (endPosition != valueRaw.size()) {
|
||||
absl::string_view excess(&valueRaw[endPosition], valueRaw.size() - endPosition);
|
||||
reader.putBackChars(excess);
|
||||
valueRaw.resize(position);
|
||||
|
||||
// ensure that we are landing back next to a space char
|
||||
if (hitChar == '=' && !reader.hasOneOfChars(" \t\r\n")) {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ valueStart, end }, "Unexpected `=` in opcode value.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
valueRaw.resize(endPosition);
|
||||
}
|
||||
|
||||
while (!valueRaw.empty() && isSpaceChar(valueRaw.back())) {
|
||||
reader.putBackChar(valueRaw.back());
|
||||
valueRaw.pop_back();
|
||||
}
|
||||
SourceLocation valueEnd = reader.location();
|
||||
|
||||
if (!_currentHeader)
|
||||
|
|
@ -444,34 +466,53 @@ size_t Parser::extractToEol(Reader& reader, std::string* dst)
|
|||
std::string Parser::expandDollarVars(const SourceRange& range, absl::string_view src)
|
||||
{
|
||||
std::string dst;
|
||||
std::string srcbuf; // temporary for retries when recursive
|
||||
std::string name; // temporary for variable name
|
||||
bool keepExpanding = true;
|
||||
|
||||
dst.reserve(2 * src.size());
|
||||
name.reserve(64);
|
||||
|
||||
size_t i = 0;
|
||||
size_t n = src.size();
|
||||
while (i < n) {
|
||||
char c = src[i++];
|
||||
while (keepExpanding) {
|
||||
size_t i = 0;
|
||||
size_t n = src.size();
|
||||
size_t numExpansions = 0;
|
||||
while (i < n) {
|
||||
char c = src[i++];
|
||||
|
||||
if (c != '$')
|
||||
dst.push_back(c);
|
||||
else {
|
||||
std::string name;
|
||||
name.reserve(64);
|
||||
if (c != '$')
|
||||
dst.push_back(c);
|
||||
else {
|
||||
++numExpansions;
|
||||
name.clear();
|
||||
|
||||
while (i < n && isIdentifierChar(src[i]))
|
||||
name.push_back(src[i++]);
|
||||
// ARIA: we will accumulate any chars after $, until this is the
|
||||
// name of a known variable
|
||||
auto def = _currentDefinitions.end();
|
||||
while (i < n && isIdentifierChar(src[i]) && def == _currentDefinitions.end()) {
|
||||
name.push_back(src[i++]);
|
||||
def = _currentDefinitions.find(name);
|
||||
}
|
||||
|
||||
if (name.empty()) {
|
||||
emitWarning(range, "Expected variable name after $.");
|
||||
continue;
|
||||
if (name.empty()) {
|
||||
emitWarning(range, "Expected variable name after $.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (def == _currentDefinitions.end()) {
|
||||
emitWarning(range, "The variable `" + name + "` is not defined.");
|
||||
continue;
|
||||
}
|
||||
|
||||
dst.append(def->second);
|
||||
}
|
||||
}
|
||||
|
||||
auto it = _currentDefinitions.find(name);
|
||||
if (it == _currentDefinitions.end()) {
|
||||
emitWarning(range, "The variable `" + name + "` is not defined.");
|
||||
continue;
|
||||
}
|
||||
|
||||
dst.append(it->second);
|
||||
keepExpanding = numExpansions > 0;
|
||||
if (keepExpanding) {
|
||||
srcbuf = dst;
|
||||
src = srcbuf;
|
||||
dst.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -339,14 +339,22 @@ TEST_CASE("[Files] sw_default and playing with switches")
|
|||
REQUIRE( synth.getRegionView(3)->isSwitchedOn() );
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("[Files] wrong (overlapping) replacement for defines")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/SpecificBugs/wrong-replacements.sfz");
|
||||
|
||||
REQUIRE( synth.getNumRegions() == 3 );
|
||||
|
||||
#if 0
|
||||
// Note: test checked to be wrong under Sforzando 1.961
|
||||
// It is the shorter matching $-variable which matches among both.
|
||||
// The rest of the variable name creates some trailing junk text
|
||||
// which Sforzando accepts without warning. (eg. `key=52Edge`)
|
||||
REQUIRE( synth.getRegionView(0)->keyRange.getStart() == 52 );
|
||||
REQUIRE( synth.getRegionView(0)->keyRange.getEnd() == 52 );
|
||||
#endif
|
||||
|
||||
REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 );
|
||||
REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 );
|
||||
REQUIRE(!synth.getRegionView(2)->amplitudeCC.empty());
|
||||
|
|
|
|||
|
|
@ -523,3 +523,196 @@ param3=baz param4=quux /* block comment */)");
|
|||
REQUIRE(mock.fullBlockHeaders == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockMembers == expectedMembers);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Overlapping definition identifiers")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/overlappingDefinitionIdentifiers.sfz",
|
||||
R"(#define $abc foo
|
||||
#define $abcdef bar
|
||||
<region> sample=$abc.wav
|
||||
<region> sample=$abcdef.wav)");
|
||||
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "foo.wav"}},
|
||||
{{"sample", "foodef.wav"}},
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region", "region"
|
||||
};
|
||||
std::vector<sfz::Opcode> 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);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Interpretation of the value of #define")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/defineValues.sfz",
|
||||
R"(#define $a foo #define $b bar <region> sample=$a-$b.wav
|
||||
<region>#define $c toto sample=$c.wav)");
|
||||
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "foo-bar.wav"}},
|
||||
{{"sample", "toto.wav"}},
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region", "region"
|
||||
};
|
||||
std::vector<sfz::Opcode> 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);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Recursive expansion")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/recursiveExpansion.sfz",
|
||||
R"(#define $B foo-$A-baz
|
||||
#define $A bar
|
||||
<region> sample=$B.wav)");
|
||||
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "foo-bar-baz.wav"}},
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region"
|
||||
};
|
||||
std::vector<sfz::Opcode> 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);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Opcode value special character")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/opcodeValueSpecialCharacter.sfz",
|
||||
R"(<region>
|
||||
sample=Alto-Flute-sus-C#4-PB-loop.wav
|
||||
<region>
|
||||
sample=foo=bar<quux.wav)");
|
||||
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "Alto-Flute-sus-C#4-PB-loop.wav"}},
|
||||
{{"sample", "foo=bar<quux.wav"}},
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region", "region"
|
||||
};
|
||||
std::vector<sfz::Opcode> 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);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Opcode value with inline directives")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/opcodeValueWithInlineDirective.sfz",
|
||||
R"(<region>#define $VEL v1 sample=$VEL.wav #define $FOO bar)");
|
||||
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "v1.wav"}},
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region"
|
||||
};
|
||||
std::vector<sfz::Opcode> 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);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Opcode value with multiple consecutive spaces")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/opcodeValueWithMultipleConsecutiveSpaces.sfz",
|
||||
R"(<region> sample=foo bar baz .wav key=69 )");
|
||||
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "foo bar baz .wav"},
|
||||
{"key", "69"}},
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region"
|
||||
};
|
||||
std::vector<sfz::Opcode> 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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue