[DeckList] Refactor load from plaintext to take normalizer as param (#6664)

* [DeckList] Refactor load from plaintext to take normalizer as param

* update usages

* weaken unit test

* weaken unit test more

* revert unit test

* move CardNameNormalizer to libcockatrice_card

* update unit test

* formatting
This commit is contained in:
RickyRister
2026-03-06 10:39:04 -08:00
committed by GitHub
parent bd5cbb89d4
commit dead993639
14 changed files with 94 additions and 52 deletions

View File

@@ -8,10 +8,11 @@
#define INTERFACE_JSON_DECK_PARSER_H #define INTERFACE_JSON_DECK_PARSER_H
#include "../../../interface/deck_loader/card_node_function.h" #include "../../../interface/deck_loader/card_node_function.h"
#include "../../../interface/deck_loader/deck_loader.h"
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/deck_list/deck_list.h>
class IJsonDeckParser class IJsonDeckParser
{ {
@@ -49,7 +50,7 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
} }
deckList.loadFromStream_Plain(outStream, false); deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
deckList.forEachCard(CardNodeFunction::ResolveProviderId()); deckList.forEachCard(CardNodeFunction::ResolveProviderId());
return deckList; return deckList;
@@ -96,7 +97,7 @@ public:
outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n';
} }
deckList.loadFromStream_Plain(outStream, false); deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer());
deckList.forEachCard(CardNodeFunction::ResolveProviderId()); deckList.forEachCard(CardNodeFunction::ResolveProviderId());
QJsonObject commandersObj = obj.value("commanders").toObject(); QJsonObject commandersObj = obj.value("commanders").toObject();

View File

@@ -17,6 +17,7 @@
#include <QtConcurrentRun> #include <QtConcurrentRun>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/deck_list/deck_list.h> #include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h> #include <libcockatrice/deck_list/tree/deck_list_card_node.h>
@@ -42,7 +43,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
DeckList deckList; DeckList deckList;
switch (fmt) { switch (fmt) {
case DeckFileFormat::PlainText: case DeckFileFormat::PlainText:
result = deckList.loadFromFile_Plain(&file); result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
break; break;
case DeckFileFormat::Cockatrice: { case DeckFileFormat::Cockatrice: {
result = deckList.loadFromFile_Native(&file); result = deckList.loadFromFile_Native(&file);
@@ -50,7 +51,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo
qCInfo(DeckLoaderLog) << "Failed to load " << fileName qCInfo(DeckLoaderLog) << "Failed to load " << fileName
<< "as cockatrice format; retrying as plain format"; << "as cockatrice format; retrying as plain format";
file.seek(0); file.seek(0);
result = deckList.loadFromFile_Plain(&file); result = deckList.loadFromFile_Plain(&file, CardNameNormalizer());
fmt = DeckFileFormat::PlainText; fmt = DeckFileFormat::PlainText;
} }
break; break;

View File

@@ -14,6 +14,7 @@
#include <QPushButton> #include <QPushButton>
#include <QTextStream> #include <QTextStream>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/card/import/card_name_normalizer.h>
/** /**
* Creates the main layout and connects the signals that are common to all versions of this window * Creates the main layout and connects the signals that are common to all versions of this window
@@ -81,7 +82,7 @@ bool AbstractDlgDeckTextEdit::loadIntoDeck(DeckList &deckList) const
QTextStream stream(&buffer); QTextStream stream(&buffer);
if (deckList.loadFromStream_Plain(stream, true)) { if (deckList.loadFromStream_Plain(stream, true, CardNameNormalizer())) {
if (loadSetNameAndNumberCheckBox->isChecked()) { if (loadSetNameAndNumberCheckBox->isChecked()) {
deckList.forEachCard(CardNodeFunction::ResolveProviderId()); deckList.forEachCard(CardNodeFunction::ResolveProviderId());
} else { } else {

View File

@@ -8,6 +8,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QMessageBox> #include <QMessageBox>
#include <QNetworkReply> #include <QNetworkReply>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <version_string.h> #include <version_string.h>
DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent) DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent)
@@ -99,7 +100,7 @@ void DlgLoadDeckFromWebsite::accept()
// Parse the plain text deck here // Parse the plain text deck here
DeckList deckList; DeckList deckList;
QTextStream stream(&deckText); QTextStream stream(&deckText);
deckList.loadFromStream_Plain(stream, false); deckList.loadFromStream_Plain(stream, false, CardNameNormalizer());
deckList.forEachCard(CardNodeFunction::ResolveProviderId()); deckList.forEachCard(CardNodeFunction::ResolveProviderId());
deck = deckList; deck = deckList;

View File

@@ -14,6 +14,7 @@
#include <QDialog> #include <QDialog>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QLoggingCategory>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QVBoxLayout> #include <QVBoxLayout>

View File

@@ -2,7 +2,6 @@
#include "../../../../../deck_loader/card_node_function.h" #include "../../../../../deck_loader/card_node_function.h"
#include "../../../../../deck_loader/deck_loader.h" #include "../../../../../deck_loader/deck_loader.h"
#include "../../../../cards/card_info_picture_with_text_overlay_widget.h"
#include "../../../../cards/card_size_widget.h" #include "../../../../cards/card_size_widget.h"
#include "../../../../cards/deck_card_zone_display_widget.h" #include "../../../../cards/deck_card_zone_display_widget.h"
#include "../../../../visual_deck_editor/visual_deck_display_options_widget.h" #include "../../../../visual_deck_editor/visual_deck_display_options_widget.h"
@@ -10,7 +9,7 @@
#include "../api_response/deck/archidekt_api_response_deck.h" #include "../api_response/deck/archidekt_api_response_deck.h"
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/import/card_name_normalizer.h>
ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent,
ArchidektApiResponseDeck _response, ArchidektApiResponseDeck _response,
@@ -80,7 +79,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi
connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset);
auto decklist = QSharedPointer<DeckList>(new DeckList); auto decklist = QSharedPointer<DeckList>(new DeckList);
decklist->loadFromStream_Plain(deckStream, false); decklist->loadFromStream_Plain(deckStream, false, CardNameNormalizer());
model->setDeckList(decklist); model->setDeckList(decklist);
model->forEachCard(CardNodeFunction::ResolveProviderId()); model->forEachCard(CardNodeFunction::ResolveProviderId());

View File

@@ -1,11 +1,10 @@
#include "edhrec_deck_api_response.h" #include "edhrec_deck_api_response.h"
#include "../../../../../../deck_loader/deck_loader.h"
#include <QApplication> #include <QApplication>
#include <QDebug> #include <QDebug>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <libcockatrice/card/import/card_name_normalizer.h>
void EdhrecDeckApiResponse::fromJson(const QJsonArray &json) void EdhrecDeckApiResponse::fromJson(const QJsonArray &json)
{ {
@@ -15,7 +14,7 @@ void EdhrecDeckApiResponse::fromJson(const QJsonArray &json)
} }
QTextStream stream(&deckList); QTextStream stream(&deckList);
deck.loadFromStream_Plain(stream, true); deck.loadFromStream_Plain(stream, true, CardNameNormalizer());
} }
void EdhrecDeckApiResponse::debugPrint() const void EdhrecDeckApiResponse::debugPrint() const

View File

@@ -12,6 +12,7 @@ set(HEADERS
libcockatrice/card/database/parser/card_database_parser.h libcockatrice/card/database/parser/card_database_parser.h
libcockatrice/card/database/parser/cockatrice_xml_3.h libcockatrice/card/database/parser/cockatrice_xml_3.h
libcockatrice/card/database/parser/cockatrice_xml_4.h libcockatrice/card/database/parser/cockatrice_xml_4.h
libcockatrice/card/import/card_name_normalizer.h
libcockatrice/card/printing/exact_card.h libcockatrice/card/printing/exact_card.h
libcockatrice/card/printing/printing_info.h libcockatrice/card/printing/printing_info.h
libcockatrice/card/set/card_set.h libcockatrice/card/set/card_set.h
@@ -36,6 +37,7 @@ add_library(
libcockatrice/card/database/parser/card_database_parser.cpp libcockatrice/card/database/parser/card_database_parser.cpp
libcockatrice/card/database/parser/cockatrice_xml_3.cpp libcockatrice/card/database/parser/cockatrice_xml_3.cpp
libcockatrice/card/database/parser/cockatrice_xml_4.cpp libcockatrice/card/database/parser/cockatrice_xml_4.cpp
libcockatrice/card/import/card_name_normalizer.cpp
libcockatrice/card/printing/exact_card.cpp libcockatrice/card/printing/exact_card.cpp
libcockatrice/card/printing/printing_info.cpp libcockatrice/card/printing/printing_info.cpp
libcockatrice/card/relation/card_relation.cpp libcockatrice/card/relation/card_relation.cpp

View File

@@ -0,0 +1,45 @@
#include "card_name_normalizer.h"
#include <QRegularExpression>
QString CardNameNormalizer::operator()(const QString &cardNameString) const
{
QString cardName = cardNameString;
// Regex for advanced card parsing
static const QRegularExpression reSplitCard(R"( ?\/\/ ?)");
static const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested
static const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string
static const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits
static const QRegularExpression reBraceDigit(
R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number
static const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)");
static const QHash<QRegularExpression, QString> differences{{QRegularExpression(""), "'"},
{QRegularExpression("Æ"), "Ae"},
{QRegularExpression("æ"), "ae"},
{QRegularExpression(" ?[|/]+ ?"), " // "}};
// Handle advanced card types
if (cardName.contains(reSplitCard)) {
cardName = cardName.split(reSplitCard).join(" // ");
}
if (cardName.contains(reDoubleFacedMarker)) {
QStringList faces = cardName.split(reDoubleFacedMarker);
cardName = faces.first().trimmed();
}
// Remove unnecessary characters
cardName.remove(reBrace);
cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards
cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end
cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after
// Normalize characters
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
cardName.replace(diff.key(), diff.value());
}
return cardName;
}

View File

@@ -0,0 +1,15 @@
#ifndef COCKATRICE_CARD_NAME_NORMALIZER_H
#define COCKATRICE_CARD_NAME_NORMALIZER_H
#include <QString>
/**
* Functor that normalizes the raw card name parsed during a plaintext deck import into the card name that Cockatrice
* uses.
*/
struct CardNameNormalizer
{
QString operator()(const QString &cardNameString) const;
};
#endif // COCKATRICE_CARD_NAME_NORMALIZER_H

View File

@@ -199,9 +199,12 @@ bool DeckList::saveToFile_Native(QIODevice *device) const
* *
* @param in The text to load * @param in The text to load
* @param preserveMetadata If true, don't clear the existing metadata * @param preserveMetadata If true, don't clear the existing metadata
* @param cardNameNormalizer Function that takes the parsed card name string in the text and
* @return False if the input was empty, true otherwise. * @return False if the input was empty, true otherwise.
*/ */
bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) bool DeckList::loadFromStream_Plain(QTextStream &in,
bool preserveMetadata,
const std::function<QString(const QString &)> &cardNameNormalizer)
{ {
const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption);
const QRegularExpression reEmpty("^\\s*$"); const QRegularExpression reEmpty("^\\s*$");
@@ -213,23 +216,11 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
// Regex for advanced card parsing // Regex for advanced card parsing
const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))"); const QRegularExpression reMultiplier(R"(^[xX\(\[]*(\d+)[xX\*\)\]]* ?(.+))");
const QRegularExpression reSplitCard(R"( ?\/\/ ?)");
const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested
const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string
const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits
const QRegularExpression reBraceDigit(
R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number
const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)");
// Regex for extracting set code and collector number with attached symbols // 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 reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))");
const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))");
const QHash<QRegularExpression, QString> differences{{QRegularExpression(""), QString("'")},
{QRegularExpression("Æ"), QString("Ae")},
{QRegularExpression("æ"), QString("ae")},
{QRegularExpression(" ?[|/]+ ?"), QString(" // ")}};
cleanList(preserveMetadata); cleanList(preserveMetadata);
auto inputs = in.readAll().trimmed().split('\n'); auto inputs = in.readAll().trimmed().split('\n');
@@ -355,26 +346,8 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
cardName = match.captured(2); cardName = match.captured(2);
} }
// Handle advanced card types // Normalize the card name
if (cardName.contains(reSplitCard)) { cardName = cardNameNormalizer(cardName);
cardName = cardName.split(reSplitCard).join(" // ");
}
if (cardName.contains(reDoubleFacedMarker)) {
QStringList faces = cardName.split(reDoubleFacedMarker);
cardName = faces.first().trimmed();
}
// Remove unnecessary characters
cardName.remove(reBrace);
cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards
cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end
cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after
// Normalize names
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
cardName.replace(diff.key(), diff.value());
}
// Determine the zone (mainboard/sideboard) // Determine the zone (mainboard/sideboard)
QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN;
@@ -387,10 +360,10 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata)
return true; return true;
} }
bool DeckList::loadFromFile_Plain(QIODevice *device) bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function<QString(const QString &)> &cardNameNormalizer)
{ {
QTextStream in(device); QTextStream in(device);
return loadFromStream_Plain(in, false); return loadFromStream_Plain(in, false, cardNameNormalizer);
} }
bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const bool DeckList::saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const

View File

@@ -206,8 +206,10 @@ public:
/// @name Serialization (Plain text) /// @name Serialization (Plain text)
///@{ ///@{
bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata); bool loadFromStream_Plain(QTextStream &stream,
bool loadFromFile_Plain(QIODevice *device); bool preserveMetadata,
const std::function<QString(const QString &)> &cardNameNormalizer);
bool loadFromFile_Plain(QIODevice *device, const std::function<QString(const QString &)> &cardNameNormalizer);
bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const; bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const;
bool bool
saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const;

View File

@@ -10,6 +10,7 @@ set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VE
) )
target_link_libraries( target_link_libraries(
loading_from_clipboard_test libcockatrice_deck_list Threads::Threads ${GTEST_BOTH_LIBRARIES} ${TEST_QT_MODULES} loading_from_clipboard_test libcockatrice_deck_list libcockatrice_card Threads::Threads ${GTEST_BOTH_LIBRARIES}
${TEST_QT_MODULES}
) )
add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test) add_test(NAME loading_from_clipboard_test COMMAND loading_from_clipboard_test)

View File

@@ -1,6 +1,7 @@
#include "clipboard_testing.h" #include "clipboard_testing.h"
#include <QTextStream> #include <QTextStream>
#include <libcockatrice/card/import/card_name_normalizer.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h> #include <libcockatrice/deck_list/tree/deck_list_card_node.h>
DeckList getDeckList(const QString &clipboard) DeckList getDeckList(const QString &clipboard)
@@ -8,7 +9,7 @@ DeckList getDeckList(const QString &clipboard)
DeckList deckList; DeckList deckList;
QString cp(clipboard); QString cp(clipboard);
QTextStream stream(&cp); // text stream requires local copy QTextStream stream(&cp); // text stream requires local copy
deckList.loadFromStream_Plain(stream, false); deckList.loadFromStream_Plain(stream, false, CardNameNormalizer());
return deckList; return deckList;
} }