This commit is contained in:
BruebachL 2026-09-12 20:29:18 -07:00 committed by GitHub
commit 566148973e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 60 additions and 7 deletions

View file

@ -38,7 +38,18 @@ Selecting this action will open a new text editor dialog with the contents of yo
The import dialog expects each line to be a card with the following format:
TODO
`[quantity] Card Name [options]`
- `quantity` is the number of copies and can be written as `2`, `2x` or `2X`. If omitted, a single copy is imported.
- `options` are appended after the card name and are understood on a best-effort basis:
- a set code and collector number, either as `Card Name (SET) 123` or `Card Name (SET) 123-1`
- a foil marker, `Card Name *F*`
- Lines can also carry a `SB:` prefix or a set code prefix (`[SET]`) — Cockatrice tries to stay compatible with the most
common deck formats.
Tab-separated exports from third-party apps (such as Delver Lens) are also accepted: any line whose first column is a
plain quantity treats the following columns as the card name and printer/set metadata (which is discarded), for example
`2\tCard Name\tSet Name`.
Each card should be on a separate line and there should be no empty lines between cards. The first empty line between
two blocks of cards will be considered as the divider between mainboard and sideboard.

View file

@ -258,6 +258,7 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
// Regex for advanced card parsing
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
const QRegularExpression reTabQuantity(R"(^\d+[xX]?$)");
// Regex for extracting set code and collector number with attached symbols
const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
@ -337,8 +338,34 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
continue;
}
QString cardName = match.captured().simplified();
const QString rawLine = match.captured();
QString cardName = rawLine.simplified();
bool sideboard = false;
int amount = 1;
// Card names never contain tab characters, so a leading numeric field
// marks a tabular record emitted by third-party deck tools (e.g. Delver
// Lens "2\tCard Name\tSet Name"). The first two columns are the quantity
// and the card name; any further columns are printer/set metadata and
// are discarded.
bool hasTabQuantity = false;
const auto tabFields = rawLine.split('\t');
int firstField = 0;
while (firstField < tabFields.size() && tabFields.at(firstField).trimmed().isEmpty()) {
++firstField;
}
if (firstField + 1 < tabFields.size()) {
const QString quantity = tabFields.at(firstField).trimmed();
if (reTabQuantity.match(quantity).hasMatch()) {
hasTabQuantity = true;
QString digits = quantity;
if (digits.endsWith('x') || digits.endsWith('X')) {
digits.chop(1);
}
amount = digits.toInt();
cardName = tabFields.at(firstField + 1).simplified();
}
}
// Sideboard detection
if (sBStart < 0) {
@ -383,11 +410,12 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
}
// check if a specific amount is mentioned
int amount = 1;
match = reMultiplier.match(cardName);
if (match.hasMatch()) {
amount = match.captured(1).toInt();
cardName = match.captured(2);
if (!hasTabQuantity) {
match = reMultiplier.match(cardName);
if (match.hasMatch()) {
amount = match.captured(1).toInt();
cardName = match.captured(2);
}
}
// Normalize the card name

View file

@ -74,6 +74,20 @@ TEST(LoadingFromClipboardTest, WeirdWhitespaceIsIgnored)
testDeck(clipboard, result);
}
TEST(LoadingFromClipboardTest, TabSeparatedImport)
{
QString clipboard("2\tMystic Snake\tStreets of New Capenna\n"
"3x\tCounterspell\tNPH\n"
"\t2\tSol Ring\tFMB\n"
"1\tForest\n"
"\n"
"2x\tDoom Blade\tM11\n");
Result result("", "", {{"Mystic Snake", 2}, {"Counterspell", 3}, {"Sol Ring", 2}, {"Forest", 1}},
{{"Doom Blade", 2}});
testDeck(clipboard, result);
}
TEST(LoadingFromClipboardTest, RemoveBlankEntriesFromBeginningAndEnd)
{
QString clipboard("\n"