mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[DeckList] Extract plain-text deck parser into own file (#7305)
* [DeckList] Extract plain-text deck parser into own file DeckList::loadFromStream_Plain was a 160-line god-method mixing deck clearing, name/comment detection, sideboard heuristics, set and multiplier extraction and normalization. The parsing logic moves verbatim into DeckListPlainText::parse() so it lives in a dedicated, testable unit; DeckList keeps a thin delegating wrapper and still refreshes the deck hash exactly as before (also on the empty-input path, to match cleanList's original behavior). The *F* foil suffix handling is relocated unchanged. * [DeckList] Harden plain-text parser regexes and move metadata clearing up --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
f4d5fc181d
commit
69d32ed853
4 changed files with 212 additions and 153 deletions
|
|
@ -11,6 +11,7 @@ set(HEADERS
|
||||||
libcockatrice/deck_list/deck_list_history_manager.h
|
libcockatrice/deck_list/deck_list_history_manager.h
|
||||||
libcockatrice/deck_list/deck_list_node_tree.h
|
libcockatrice/deck_list/deck_list_node_tree.h
|
||||||
libcockatrice/deck_list/deck_list_memento.h
|
libcockatrice/deck_list/deck_list_memento.h
|
||||||
|
libcockatrice/deck_list/deck_list_plain_text_parser.h
|
||||||
libcockatrice/deck_list/playmat_resolver.h
|
libcockatrice/deck_list/playmat_resolver.h
|
||||||
libcockatrice/deck_list/sideboard_plan.h
|
libcockatrice/deck_list/sideboard_plan.h
|
||||||
)
|
)
|
||||||
|
|
@ -27,6 +28,7 @@ add_library(
|
||||||
libcockatrice/deck_list/deck_list.cpp
|
libcockatrice/deck_list/deck_list.cpp
|
||||||
libcockatrice/deck_list/deck_list_history_manager.cpp
|
libcockatrice/deck_list/deck_list_history_manager.cpp
|
||||||
libcockatrice/deck_list/deck_list_node_tree.cpp
|
libcockatrice/deck_list/deck_list_node_tree.cpp
|
||||||
|
libcockatrice/deck_list/deck_list_plain_text_parser.cpp
|
||||||
libcockatrice/deck_list/playmat_resolver.cpp
|
libcockatrice/deck_list/playmat_resolver.cpp
|
||||||
libcockatrice/deck_list/sideboard_plan.cpp
|
libcockatrice/deck_list/sideboard_plan.cpp
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "deck_list.h"
|
#include "deck_list.h"
|
||||||
|
|
||||||
#include "deck_list_memento.h"
|
#include "deck_list_memento.h"
|
||||||
|
#include "deck_list_plain_text_parser.h"
|
||||||
#include "tree/abstract_deck_list_node.h"
|
#include "tree/abstract_deck_list_node.h"
|
||||||
#include "tree/deck_list_card_node.h"
|
#include "tree/deck_list_card_node.h"
|
||||||
#include "tree/inner_deck_list_node.h"
|
#include "tree/inner_deck_list_node.h"
|
||||||
|
|
@ -8,7 +9,6 @@
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
@ -250,160 +250,12 @@ bool DeckList::loadFromStream_Plain(QTextStream &in,
|
||||||
bool preserveMetadata,
|
bool preserveMetadata,
|
||||||
const std::function<QString(const QString &)> &cardNameNormalizer)
|
const std::function<QString(const QString &)> &cardNameNormalizer)
|
||||||
{
|
{
|
||||||
const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
|
if (!preserveMetadata) {
|
||||||
const QRegularExpression reEmpty("^\\s*$");
|
metadata = {};
|
||||||
const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
|
|
||||||
const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption);
|
|
||||||
const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption);
|
|
||||||
const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b",
|
|
||||||
QRegularExpression::CaseInsensitiveOption);
|
|
||||||
|
|
||||||
// Regex for advanced card parsing
|
|
||||||
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\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]*))");
|
|
||||||
const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))");
|
|
||||||
|
|
||||||
cleanList(preserveMetadata);
|
|
||||||
|
|
||||||
auto inputs = in.readAll().trimmed().split('\n');
|
|
||||||
auto max_line = inputs.size();
|
|
||||||
|
|
||||||
// Start at the first empty line before the first card line
|
|
||||||
auto deckStart = inputs.indexOf(reCardLine);
|
|
||||||
if (deckStart == -1) {
|
|
||||||
if (inputs.indexOf(reComment) == -1) {
|
|
||||||
return false; // Input is empty
|
|
||||||
}
|
|
||||||
deckStart = max_line;
|
|
||||||
} else {
|
|
||||||
deckStart = inputs.lastIndexOf(reEmpty, deckStart);
|
|
||||||
if (deckStart == -1) {
|
|
||||||
deckStart = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
bool ok = DeckListPlainText::parse(in, cardNameNormalizer, metadata, tree);
|
||||||
// find sideboard position, if marks are used this won't be needed
|
|
||||||
int sBStart = -1;
|
|
||||||
if (inputs.indexOf(reSBMark, deckStart) == -1) {
|
|
||||||
sBStart = inputs.indexOf(reSBComment, deckStart);
|
|
||||||
if (sBStart == -1) {
|
|
||||||
sBStart = inputs.indexOf(reEmpty, deckStart + 1);
|
|
||||||
if (sBStart == -1) {
|
|
||||||
sBStart = max_line;
|
|
||||||
}
|
|
||||||
auto nextCard = inputs.indexOf(reCardLine, sBStart + 1);
|
|
||||||
if (inputs.indexOf(reEmpty, nextCard + 1) != -1) {
|
|
||||||
sBStart = max_line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
QRegularExpressionMatch match;
|
|
||||||
|
|
||||||
// Parse name and comments
|
|
||||||
while (index < deckStart) {
|
|
||||||
const auto ¤t = inputs.at(index++);
|
|
||||||
if (!current.contains(reEmpty)) {
|
|
||||||
match = reComment.match(current);
|
|
||||||
metadata.name = match.captured();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (index < deckStart) {
|
|
||||||
const auto ¤t = inputs.at(index++);
|
|
||||||
if (!current.contains(reEmpty)) {
|
|
||||||
match = reComment.match(current);
|
|
||||||
metadata.comments += match.captured() + '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metadata.comments.chop(1);
|
|
||||||
|
|
||||||
// Discard empty lines
|
|
||||||
while (index < max_line && inputs.at(index).contains(reEmpty)) {
|
|
||||||
++index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard
|
|
||||||
if (inputs.at(index).contains(reDeckComment)) {
|
|
||||||
++index;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse decklist
|
|
||||||
for (; index < max_line; ++index) {
|
|
||||||
// check if line is a card
|
|
||||||
match = reCardLine.match(inputs.at(index));
|
|
||||||
if (!match.hasMatch()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString cardName = match.captured().simplified();
|
|
||||||
bool sideboard = false;
|
|
||||||
|
|
||||||
// Sideboard detection
|
|
||||||
if (sBStart < 0) {
|
|
||||||
match = reSBMark.match(cardName);
|
|
||||||
if (match.hasMatch()) {
|
|
||||||
sideboard = true;
|
|
||||||
cardName = match.captured(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (index == sBStart) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
sideboard = index > sBStart;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract set code, collector number, and foil
|
|
||||||
QString setCode;
|
|
||||||
QString collectorNumber;
|
|
||||||
bool isFoil = false;
|
|
||||||
|
|
||||||
// Check for foil status at the end of the card name
|
|
||||||
if (cardName.endsWith("*F*", Qt::CaseInsensitive)) {
|
|
||||||
isFoil = true;
|
|
||||||
cardName.chop(3); // Remove the "*F*" from the card name
|
|
||||||
}
|
|
||||||
Q_UNUSED(isFoil);
|
|
||||||
|
|
||||||
// Attempt to match the hyphen-separated format (PLST-2094)
|
|
||||||
match = reHyphenFormat.match(cardName);
|
|
||||||
if (match.hasMatch()) {
|
|
||||||
setCode = match.captured(2).toUpper();
|
|
||||||
collectorNumber = match.captured(3);
|
|
||||||
cardName = cardName.left(match.capturedStart()).trimmed();
|
|
||||||
} else {
|
|
||||||
// Attempt to match the regular format (PLST) 2094
|
|
||||||
match = reRegularFormat.match(cardName);
|
|
||||||
if (match.hasMatch()) {
|
|
||||||
setCode = match.captured(1).toUpper();
|
|
||||||
collectorNumber = match.captured(2);
|
|
||||||
cardName = cardName.left(match.capturedStart()).trimmed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize the card name
|
|
||||||
cardName = cardNameNormalizer(cardName);
|
|
||||||
|
|
||||||
// Determine the zone (mainboard/sideboard)
|
|
||||||
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
|
|
||||||
|
|
||||||
// make new entry in decklist
|
|
||||||
tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshDeckHash();
|
refreshDeckHash();
|
||||||
return true;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function<QString(const QString &)> &cardNameNormalizer)
|
bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function<QString(const QString &)> &cardNameNormalizer)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
#include "deck_list_plain_text_parser.h"
|
||||||
|
|
||||||
|
#include "deck_list_node_tree.h"
|
||||||
|
#include "tree/inner_deck_list_node.h"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
namespace DeckListPlainText
|
||||||
|
{
|
||||||
|
|
||||||
|
bool parse(QTextStream &in,
|
||||||
|
const std::function<QString(const QString &)> &cardNameNormalizer,
|
||||||
|
DeckList::Metadata &metadata,
|
||||||
|
DecklistNodeTree &tree)
|
||||||
|
{
|
||||||
|
tree.clear();
|
||||||
|
|
||||||
|
static const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
static const QRegularExpression reEmpty("^\\s*$");
|
||||||
|
static const QRegularExpression reComment(R"([\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
|
||||||
|
static const QRegularExpression reSBMark("^\\s*sb:\\s*(.+)", QRegularExpression::CaseInsensitiveOption);
|
||||||
|
static const QRegularExpression reSBComment("^sideboard\\b.*$", QRegularExpression::CaseInsensitiveOption);
|
||||||
|
static const QRegularExpression reDeckComment("^((main)?deck(list)?|mainboard)\\b",
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
|
// Regex for advanced card parsing
|
||||||
|
static const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\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]*))");
|
||||||
|
static const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))");
|
||||||
|
|
||||||
|
auto inputs = in.readAll().trimmed().split('\n');
|
||||||
|
auto max_line = inputs.size();
|
||||||
|
|
||||||
|
// Start at the first empty line before the first card line
|
||||||
|
auto deckStart = inputs.indexOf(reCardLine);
|
||||||
|
if (deckStart == -1) {
|
||||||
|
if (inputs.indexOf(reComment) == -1) {
|
||||||
|
return false; // Input is empty
|
||||||
|
}
|
||||||
|
deckStart = max_line;
|
||||||
|
} else {
|
||||||
|
deckStart = inputs.lastIndexOf(reEmpty, deckStart);
|
||||||
|
if (deckStart == -1) {
|
||||||
|
deckStart = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// find sideboard position, if marks are used this won't be needed
|
||||||
|
int sBStart = -1;
|
||||||
|
if (inputs.indexOf(reSBMark, deckStart) == -1) {
|
||||||
|
sBStart = inputs.indexOf(reSBComment, deckStart);
|
||||||
|
if (sBStart == -1) {
|
||||||
|
sBStart = inputs.indexOf(reEmpty, deckStart + 1);
|
||||||
|
if (sBStart == -1) {
|
||||||
|
sBStart = max_line;
|
||||||
|
}
|
||||||
|
auto nextCard = inputs.indexOf(reCardLine, sBStart + 1);
|
||||||
|
if (inputs.indexOf(reEmpty, nextCard + 1) != -1) {
|
||||||
|
sBStart = max_line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
QRegularExpressionMatch match;
|
||||||
|
|
||||||
|
// Parse name and comments
|
||||||
|
while (index < deckStart) {
|
||||||
|
const auto ¤t = inputs.at(index++);
|
||||||
|
if (!current.contains(reEmpty)) {
|
||||||
|
match = reComment.match(current);
|
||||||
|
metadata.name = match.captured();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (index < deckStart) {
|
||||||
|
const auto ¤t = inputs.at(index++);
|
||||||
|
if (!current.contains(reEmpty)) {
|
||||||
|
match = reComment.match(current);
|
||||||
|
metadata.comments += match.captured() + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata.comments.chop(1);
|
||||||
|
|
||||||
|
// Discard empty lines
|
||||||
|
while (index < max_line && inputs.at(index).contains(reEmpty)) {
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard line if it starts with deck or mainboard, all cards until the sideboard starts are in the mainboard
|
||||||
|
if (inputs.at(index).contains(reDeckComment)) {
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse decklist
|
||||||
|
for (; index < max_line; ++index) {
|
||||||
|
// check if line is a card
|
||||||
|
match = reCardLine.match(inputs.at(index));
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString cardName = match.captured().simplified();
|
||||||
|
bool sideboard = false;
|
||||||
|
|
||||||
|
// Sideboard detection
|
||||||
|
if (sBStart < 0) {
|
||||||
|
match = reSBMark.match(cardName);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
sideboard = true;
|
||||||
|
cardName = match.captured(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index == sBStart) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sideboard = index > sBStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract set code, collector number, and foil
|
||||||
|
QString setCode;
|
||||||
|
QString collectorNumber;
|
||||||
|
bool isFoil = false;
|
||||||
|
|
||||||
|
// Check for foil status at the end of the card name
|
||||||
|
if (cardName.endsWith("*F*", Qt::CaseInsensitive)) {
|
||||||
|
isFoil = true;
|
||||||
|
cardName.chop(3); // Remove the "*F*" from the card name
|
||||||
|
}
|
||||||
|
Q_UNUSED(isFoil);
|
||||||
|
|
||||||
|
// Attempt to match the hyphen-separated format (PLST-2094)
|
||||||
|
match = reHyphenFormat.match(cardName);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
setCode = match.captured(2).toUpper();
|
||||||
|
collectorNumber = match.captured(3);
|
||||||
|
cardName = cardName.left(match.capturedStart()).trimmed();
|
||||||
|
} else {
|
||||||
|
// Attempt to match the regular format (PLST) 2094
|
||||||
|
match = reRegularFormat.match(cardName);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
setCode = match.captured(1).toUpper();
|
||||||
|
collectorNumber = match.captured(2);
|
||||||
|
cardName = cardName.left(match.capturedStart()).trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the card name
|
||||||
|
cardName = cardNameNormalizer(cardName);
|
||||||
|
|
||||||
|
// Determine the zone (mainboard/sideboard)
|
||||||
|
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
|
||||||
|
|
||||||
|
// make new entry in decklist
|
||||||
|
tree.addCard(cardName, amount, zoneName, -1, setCode, collectorNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace DeckListPlainText
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#ifndef COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H
|
||||||
|
#define COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H
|
||||||
|
|
||||||
|
#include "deck_list.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class QTextStream;
|
||||||
|
|
||||||
|
namespace DeckListPlainText
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parses a plain-text deck list into a tree and its metadata.
|
||||||
|
*
|
||||||
|
* Clears the tree first, then fills both from the text.
|
||||||
|
*
|
||||||
|
* @param in The text to load
|
||||||
|
* @param cardNameNormalizer Function that takes the parsed card name string
|
||||||
|
* in the text and returns the name to store
|
||||||
|
* @param metadata Deck metadata written by the parser
|
||||||
|
* @param tree Deck tree the parser adds cards to
|
||||||
|
* @return False if the input was empty, true otherwise.
|
||||||
|
*/
|
||||||
|
bool parse(QTextStream &in,
|
||||||
|
const std::function<QString(const QString &)> &cardNameNormalizer,
|
||||||
|
DeckList::Metadata &metadata,
|
||||||
|
DecklistNodeTree &tree);
|
||||||
|
|
||||||
|
} // namespace DeckListPlainText
|
||||||
|
|
||||||
|
#endif // COCKATRICE_DECK_LIST_PLAIN_TEXT_PARSER_H
|
||||||
Loading…
Add table
Add a link
Reference in a new issue