diff --git a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h index e19e4f4e4..1818aa35c 100644 --- a/cockatrice/src/client/network/parsers/interface_json_deck_parser.h +++ b/cockatrice/src/client/network/parsers/interface_json_deck_parser.h @@ -8,10 +8,11 @@ #define INTERFACE_JSON_DECK_PARSER_H #include "../../../interface/deck_loader/card_node_function.h" -#include "../../../interface/deck_loader/deck_loader.h" #include #include +#include +#include class IJsonDeckParser { @@ -49,7 +50,7 @@ public: outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; } - deckList.loadFromStream_Plain(outStream, false); + deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); return deckList; @@ -96,7 +97,7 @@ public: outStream << quantity << ' ' << cardName << " (" << setName << ") " << collectorNumber << '\n'; } - deckList.loadFromStream_Plain(outStream, false); + deckList.loadFromStream_Plain(outStream, false, CardNameNormalizer()); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); QJsonObject commandersObj = obj.value("commanders").toObject(); diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index 3481e245b..d71dca24a 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -42,7 +43,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo DeckList deckList; switch (fmt) { case DeckFileFormat::PlainText: - result = deckList.loadFromFile_Plain(&file); + result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); break; case DeckFileFormat::Cockatrice: { result = deckList.loadFromFile_Native(&file); @@ -50,7 +51,7 @@ DeckLoader::loadFromFile(const QString &fileName, DeckFileFormat::Format fmt, bo qCInfo(DeckLoaderLog) << "Failed to load " << fileName << "as cockatrice format; retrying as plain format"; file.seek(0); - result = deckList.loadFromFile_Plain(&file); + result = deckList.loadFromFile_Plain(&file, CardNameNormalizer()); fmt = DeckFileFormat::PlainText; } break; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp index b72130c81..9ceb35d78 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_clipboard.cpp @@ -14,6 +14,7 @@ #include #include #include +#include /** * 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); - if (deckList.loadFromStream_Plain(stream, true)) { + if (deckList.loadFromStream_Plain(stream, true, CardNameNormalizer())) { if (loadSetNameAndNumberCheckBox->isChecked()) { deckList.forEachCard(CardNodeFunction::ResolveProviderId()); } else { diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp index da4135246..ba00873d9 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include DlgLoadDeckFromWebsite::DlgLoadDeckFromWebsite(QWidget *parent) : QDialog(parent) @@ -99,7 +100,7 @@ void DlgLoadDeckFromWebsite::accept() // Parse the plain text deck here DeckList deckList; QTextStream stream(&deckText); - deckList.loadFromStream_Plain(stream, false); + deckList.loadFromStream_Plain(stream, false, CardNameNormalizer()); deckList.forEachCard(CardNodeFunction::ResolveProviderId()); deck = deckList; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h index fe6f0a7e9..1ac98d206 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_load_deck_from_website.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp index f40b9094f..8b17cd49e 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_display_widget.cpp @@ -2,7 +2,6 @@ #include "../../../../../deck_loader/card_node_function.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/deck_card_zone_display_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 -#include +#include ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWidget *parent, ArchidektApiResponseDeck _response, @@ -80,7 +79,7 @@ ArchidektApiResponseDeckDisplayWidget::ArchidektApiResponseDeckDisplayWidget(QWi connect(model, &DeckListModel::modelReset, this, &ArchidektApiResponseDeckDisplayWidget::decklistModelReset); auto decklist = QSharedPointer(new DeckList); - decklist->loadFromStream_Plain(deckStream, false); + decklist->loadFromStream_Plain(deckStream, false, CardNameNormalizer()); model->setDeckList(decklist); model->forEachCard(CardNodeFunction::ResolveProviderId()); diff --git a/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp b/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp index a744a9b14..1cb070c30 100644 --- a/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp @@ -1,11 +1,10 @@ #include "edhrec_deck_api_response.h" -#include "../../../../../../deck_loader/deck_loader.h" - #include #include #include #include +#include void EdhrecDeckApiResponse::fromJson(const QJsonArray &json) { @@ -15,7 +14,7 @@ void EdhrecDeckApiResponse::fromJson(const QJsonArray &json) } QTextStream stream(&deckList); - deck.loadFromStream_Plain(stream, true); + deck.loadFromStream_Plain(stream, true, CardNameNormalizer()); } void EdhrecDeckApiResponse::debugPrint() const diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index f516cde00..dd3799e33 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -12,6 +12,7 @@ set(HEADERS libcockatrice/card/database/parser/card_database_parser.h libcockatrice/card/database/parser/cockatrice_xml_3.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/printing_info.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/cockatrice_xml_3.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/printing_info.cpp libcockatrice/card/relation/card_relation.cpp diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp new file mode 100644 index 000000000..2349dc3e8 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.cpp @@ -0,0 +1,45 @@ +#include "card_name_normalizer.h" + +#include + +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 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; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h new file mode 100644 index 000000000..716e7da80 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/import/card_name_normalizer.h @@ -0,0 +1,15 @@ +#ifndef COCKATRICE_CARD_NAME_NORMALIZER_H +#define COCKATRICE_CARD_NAME_NORMALIZER_H + +#include + +/** + * 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 diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 3d1070f15..e3e7b41c0 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -199,9 +199,12 @@ bool DeckList::saveToFile_Native(QIODevice *device) const * * @param in The text to load * @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. */ -bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) +bool DeckList::loadFromStream_Plain(QTextStream &in, + bool preserveMetadata, + const std::function &cardNameNormalizer) { const QRegularExpression reCardLine(R"(^\s*[\w\[\(\{].*$)", QRegularExpression::UseUnicodePropertiesOption); const QRegularExpression reEmpty("^\\s*$"); @@ -213,23 +216,11 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) // Regex for advanced card parsing 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 const QRegularExpression reHyphenFormat(R"(\((\w{3,})\)\s+(\w{3,})-(\d+[^\w\s]*))"); const QRegularExpression reRegularFormat(R"(\((\w{3,})\)\s+(\d+[^\w\s]*))"); - const QHash differences{{QRegularExpression("’"), QString("'")}, - {QRegularExpression("Æ"), QString("Ae")}, - {QRegularExpression("æ"), QString("ae")}, - {QRegularExpression(" ?[|/]+ ?"), QString(" // ")}}; - cleanList(preserveMetadata); auto inputs = in.readAll().trimmed().split('\n'); @@ -355,26 +346,8 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) cardName = match.captured(2); } - // 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 names - for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) { - cardName.replace(diff.key(), diff.value()); - } + // Normalize the card name + cardName = cardNameNormalizer(cardName); // Determine the zone (mainboard/sideboard) QString zoneName = sideboard ? DECK_ZONE_SIDE : DECK_ZONE_MAIN; @@ -387,10 +360,10 @@ bool DeckList::loadFromStream_Plain(QTextStream &in, bool preserveMetadata) return true; } -bool DeckList::loadFromFile_Plain(QIODevice *device) +bool DeckList::loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer) { 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 diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 91e56fc9f..a96adeb38 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -206,8 +206,10 @@ public: /// @name Serialization (Plain text) ///@{ - bool loadFromStream_Plain(QTextStream &stream, bool preserveMetadata); - bool loadFromFile_Plain(QIODevice *device); + bool loadFromStream_Plain(QTextStream &stream, + bool preserveMetadata, + const std::function &cardNameNormalizer); + bool loadFromFile_Plain(QIODevice *device, const std::function &cardNameNormalizer); bool saveToStream_Plain(QTextStream &stream, bool prefixSideboardCards, bool slashTappedOutSplitCards) const; bool saveToFile_Plain(QIODevice *device, bool prefixSideboardCards = true, bool slashTappedOutSplitCards = false) const; diff --git a/tests/loading_from_clipboard/CMakeLists.txt b/tests/loading_from_clipboard/CMakeLists.txt index 40e85c66e..eecaafcbb 100644 --- a/tests/loading_from_clipboard/CMakeLists.txt +++ b/tests/loading_from_clipboard/CMakeLists.txt @@ -10,6 +10,7 @@ set(TEST_QT_MODULES ${COCKATRICE_QT_VERSION_NAME}::Concurrent ${COCKATRICE_QT_VE ) 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) diff --git a/tests/loading_from_clipboard/clipboard_testing.cpp b/tests/loading_from_clipboard/clipboard_testing.cpp index 29535c1dc..a9977b800 100644 --- a/tests/loading_from_clipboard/clipboard_testing.cpp +++ b/tests/loading_from_clipboard/clipboard_testing.cpp @@ -1,6 +1,7 @@ #include "clipboard_testing.h" #include +#include #include DeckList getDeckList(const QString &clipboard) @@ -8,7 +9,7 @@ DeckList getDeckList(const QString &clipboard) DeckList deckList; QString cp(clipboard); QTextStream stream(&cp); // text stream requires local copy - deckList.loadFromStream_Plain(stream, false); + deckList.loadFromStream_Plain(stream, false, CardNameNormalizer()); return deckList; }