Support tab-separated deck imports (#4222) (#7267)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

Third-party deck tools (e.g. Delver Lens) export decks as tab-separated rows
such as "2	Card Name	Set Name". Card names never contain tab characters,
so a leading numeric column marks a tabular record: the first two columns are read
as the quantity and card name, and any further columns are discarded as
printer/set metadata.

- deck_list: parse tab-delimited rows before whitespace collapsing
- tests: cover tab-separated mainboard and sideboard imports
- docs: document the accepted plain-text import format (fills the TODO)

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-11 21:19:10 +02:00 • committed by GitHub
parent 8968709642
commit 96b819d7c8
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

@ -33,6 +33,7 @@ bool parse(QTextStream &in,
// Regex for advanced card parsing
static const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
static const QRegularExpression reTabQuantity(R"(^\d+[xX]?$)");
// Regex for extracting set code and collector number with attached symbols
static const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
@ -110,8 +111,34 @@ bool parse(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) {
@ -156,11 +183,12 @@ bool parse(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

@ -80,6 +80,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"