Repository navigation
Decklist Parsing
English · Español
Proxy Builder turns the text a user pastes (or an Archidekt URL) into a structured list of lines, and later turns the finished deck back into text for Moxfield, Arena, MTGO or plain text. Both directions live in the pure crate proxy-core (src-tauri/crates/proxy-core/src/decklist/), so they have no network, no Tauri and no database; the Tauri layer only wraps them in commands.
Boundaries: this page covers reading and writing list text. Looking the cards up (Scryfall, languages, images) is Scryfall-Integration; what happens to the resulting cards in the deck is Deck-Model.
| File | Responsibility |
|---|---|
src-tauri/crates/proxy-core/src/decklist/mod.rs |
parse(), ParsedDecklist, ParsedLine, Section, LineKind, LineError; the grammar. |
src-tauri/crates/proxy-core/src/decklist/exportar.rs |
exportar() and FormatoLista: deck -> text for Moxfield / Arena / MTGO / plain text. |
src-tauri/src/commands/decklist.rs |
Tauri commands parse_decklist, deck_list_text, deck_list_save. |
src-tauri/src/archidekt.rs |
id_de_url(), desde_json(): import of a public Archidekt deck by URL. |
src-tauri/src/commands/cards.rs |
import_archidekt command (does the HTTP call, then calls archidekt::desde_json). |
src/features/decklist/editor-lista.tsx |
The list text box: calls parse_decklist 250 ms after the last keystroke and marks invalid lines. |
src/features/decklist/acciones.ts |
cargarLista(): parse_decklist then resolve_decklist. |
src/features/export/menu-exportar.tsx |
The "..." menu next to "Export PDF": copy/save the list. |
The parser never guesses silently: every input line comes back as a ParsedLine with a kind, and a line that cannot be understood carries a LineError code so the UI can mark it. The UI translates the code; Rust never writes prose (see IPC-and-Error-Handling).
All types derive TS with #[ts(export)] and are generated into src/bindings/ by pnpm bindings.
ParsedDecklist:
| Field | Meaning |
|---|---|
lines: Vec<ParsedLine> |
One entry per input line (blank and comment lines included), in order. |
card_count: u32 |
Sum of qty of lines that are Card and included. |
invalid_count: u32 |
Number of Invalid lines. |
ParsedLine:
| Field | Meaning |
|---|---|
line_no |
1-based line number. |
raw |
The original text, untouched. |
kind |
LineKind: Card, Section, Comment, Empty, Invalid. |
qty, name
|
Copies and cleaned card name (only meaningful for Card). |
set, collector
|
Lower-cased set code (cmm) and collector number, if the line had them. |
foil |
true if the line carried a foil/etched/commander mark (see "Marks"). |
section |
Section: Main, Commander, Sideboard, Companion, Maybeboard, Tokens. |
included |
false for the Maybeboard and for Archidekt categories flagged {noDeck}. Only included lines are resolved. |
error |
LineError: ZeroQuantity, BadQuantity, TooMany (> 999 copies, MAX_COPIAS), EmptyName. |
Section::included_by_default() is true for everything except Maybeboard.
The module header documents the intent: accept whatever the reference site accepts, plus the exports of the common sites. Concretely (all covered by formatos_de_arena_moxfield_y_archidekt and neighbours in the mod.rs tests):
| Source | Example line |
|---|---|
| Plain text |
Sol Ring, 2x Sol Ring, 3 x Island, 2X Counterspell
|
| With printing |
1 Sol Ring (CMM) 410, Bird (teoc) (set only), 1 Llanowar Elves (PLST) DOM-168, 1 Sol Ring (SLD) 2063★
|
| MTG Arena |
4 Lightning Bolt (M10) 146, About / Name My deck header block, Deck / Sideboard / Commander headers, old SB: 2 Duress prefix |
| Moxfield |
1 Fire // Ice (MH2) 290 *F*, *E*, *CMDR*, headers with counts such as Commander (1)
|
| Archidekt | 1x Sol Ring (cmm) 410 [Commander{top}] ^Have,#37d67a^ |
| Double-faced / split |
1 Delver of Secrets // Insectile Aberration (ISD) 51, 1 Who // What // When // Where // Why
|
parse(texto) walks texto.lines() once, carrying two pieces of state across lines: the current Section (starts as Main) and a flag en_about (inside an Arena "About" block). Each line goes through linea():
-
Blank line:
kind = Empty, resetsen_about. It does not change the section. -
Comment or header: if the trimmed line starts with
//or#, those characters are stripped and what remains is tested as a header; otherwise the whole line is. If it is a header (seccion_de_cabecera), the line becomesSectionand the current section changes; if it started with///#but is not a header it is aCommentand the section does not change.- Header matching lower-cases, trims a trailing
:, and drops a trailing(n)count (Commander (1),Sideboard (15)). - Recognised words:
deck,main,mainboard,main deck,maindeck,creatures,lands,spells,about->Main;commander(s)->Commander;sideboard,side,sb->Sideboard;companion(s)->Companion;maybeboard,maybe,considering->Maybeboard;tokens,token->Tokens. - Why headers are also accepted as comments: some sites paste the header as a comment when copying a list as text (
#SIDEBOARD,// Sideboard (15)); a test (cabeceras_escritas_como_comentario) pins this.
- Header matching lower-cases, trims a trailing
-
Arena About block: after an
Aboutheader, a line starting withNameis aComment(the deck's name, not a card) until the next blank line. -
SB:prefix:SB:(case-insensitive) puts only that line inSideboardwithout changing the running section. -
Archidekt suffixes (
quitar_archidekt): a trailing^...^tag block is dropped, then a trailing[...]category is read. A category containing{nodeck}or starting withmaybeboardsetsincluded = false; starting withmaybeboardalso sets the section toMaybeboard; starting withcommanderorsideboardsets that section for the line. -
Marks:
*F*,*E*,*CMDR*,*Foil*,*Etched*(case-insensitive, anywhere) are removed and setfoil = true. Quirk:*CMDR*also setsfoil(the same regex handles all of them); nothing downstream currently readsParsedLine.foil(verified: no consumer outsidedecklist/mod.rs). -
Quantity (
RE_CANTIDAD: digits, optionalx/X, then whitespace, then the rest). No leading number means quantity 1. A line that looks like a bad quantity (RE_CANTIDAD_MAL:x3 ...,3xx ..., a lone4or4x) becomesInvalidwithBadQuantity, orEmptyNamewhen it is only digits/x. Quantity 0 ->ZeroQuantity; above 999 ->TooMany. -
Set and number (
RE_SET):name (SET) [number]where SET is 2-8 alphanumerics in parentheses. The number is the next non-space token and may be anything (410,2063★,DOM-168,254p,38a). Parentheses with spaces inside (e.g. a card name with a parenthetical) do not match and stay part of the name. -
Name cleanup (
limpiar_nombre): typographic quotes become ASCII (',"), runs of whitespace collapse. An empty name isInvalid/EmptyName. - The surviving line is a
Cardwithqty,name,set,collector,section, andincluded(from the section default, possibly overridden in step 5).
- Any whole line equal to a header word is a header, even without
//or#. A card literally named like a header word on its own line would be eaten as a section change. -
Inferred from the regex: a card whose name starts with a number and a space (e.g.
1996 World Champion) is read as quantity 1996 and rejected withTooMany;7 Wonders-style names lose their number. Write1 1996 World Championinstead. -
1Island(no space) is not a quantity: it is read as a card named1Islandwith quantity 1. - Comments never change the section; blank lines do not either (only an
Aboutblock ends at a blank line). - Windows line endings are handled by
str::lines();rawkeeps the line text without the terminator. - The parser is deliberately lenient about card existence: it only checks syntax. A misspelt name parses fine and fails later as
Unresolvedin the resolver.
The Section of a line is the only place where "this card belongs to the sideboard" is known at parse time. resolve.rs copies it onto the created cards as a mark (DeckCard.marca, type MarcaCarta in src-tauri/crates/proxy-core/src/model.rs):
| Line section | Mark on the resolved copies | Effect |
|---|---|---|
Sideboard |
MarcaCarta::Banquillo |
Printed, but excluded from deck size / copies / statistics; exported under the sideboard. |
Maybeboard |
MarcaCarta::Opcional |
Printed, excluded from counts. |
| anything else | none | Counts normally. |
proxy_core::model::cuentan() is the single function that decides what counts (it also handles Variante; a design variant counts only if no unmarked copy of the same name exists). Details: Deck-Model and Statistics.
Note: resolve() only processes lines with kind == Card && included. Through parse(), a Maybeboard line has included = false, so the Opcional branch in resolve.rs is only reached if a caller sets included = true on such a line; the UI currently has no switch for that (no frontend code reads included). The Archidekt importer drops anything the site marks outside the deck for the same reason.
exportar(deck: &Deck, formato: FormatoLista) -> String builds the text from the deck's cards, not from the pasted text. FormatoLista: Moxfield, Arena, Mtgo, Texto.
How a line set is built (juntar):
- Sections are not stored on cards. They are recovered by re-parsing
deck.decklist_text(secciones_de) and looking each card up by lower-cased name; a double-faced card also matches by its front face (seccion_de). A card absent from the text defaults toMain. - Cards are filtered by the marks:
cuentan(&deck.cards)removes optional and surplus-variant copies;Banquillo-marked cards are forced intoSideboardregardless of the text. - Tokens, blanks,
Maybeboard/Tokenssections and nameless cards are skipped (va_en_la_lista). ACustomcard linked to an external source (Cockatrice token) is a token; other custom cards are skipped only if their type line says token/emblem. - Copies of the same printing in the same section are merged into one line with a count (match on section + name + set + collector number). Different printings stay on different lines.
- For double-faced cards
delanteis the front face name (Arena and MTGO want that); Moxfield and plain text keep the fullA // Bname.
Output per format (pinned by tests para_moxfield, para_arena, para_mtgo_el_comandante_va_en_el_banquillo, en_texto_plano):
| Format | Layout |
|---|---|
Moxfield |
Commander + companion + main as one block of N Name (SET) number, then a blank line, SIDEBOARD: and the sideboard. |
Arena |
Headed blocks Commander, Companion, Deck, Sideboard (empty blocks omitted), front-face names, (SET) number. |
Mtgo |
N Name with the main deck first, then a blank line, then commander/companion/sideboard together (MTGO has no commander slot). |
Texto |
N Name, commander+main together, a blank line, then the sideboard. |
Set codes are upper-cased on export. A set without number prints (SET) only.
| Command | Signature | Notes |
|---|---|---|
parse_decklist |
(text: String) -> ParsedDecklist |
Synchronous and cheap; called while typing (debounced 250 ms in editor-lista.tsx) and again by cargarLista(). |
deck_list_text |
(deck: Deck, format: FormatoLista) -> String |
Used by the "copy list" entries of menu-exportar.tsx. |
deck_list_save |
(deck, format, path) -> () |
Path must be absolute and end in .txt (else InvalidInput); written with escribir_atomico. The UI uses it for "Save for MTGO". |
import_archidekt |
(url: String, include_sideboard: bool) -> ArchidektImport { text, list } |
See below. |
sequenceDiagram
participant UI as decklist-panel.tsx
participant C as import_archidekt (cards.rs)
participant A as archidekt.rs
participant W as archidekt.com
UI->>C: url, includeSideboard
C->>A: id_de_url(url)
A-->>C: deck id (or None -> InvalidInput bad_url)
C->>W: GET /api/decks/{id}/
W-->>C: JSON (404/403 -> NotFound)
C->>A: desde_json(json, include_sideboard)
A-->>C: Importada { texto, lista, ids }
C-->>UI: ArchidektImport { text, list }
UI->>UI: put text in the list box (no history entry)
-
id_de_urlacceptsarchidekt.com/decks/<id>/...,www., with or without scheme, and.../api/decks/<id>/. Anything else returnsNone, which the command reports asInvalidInputwithmotivo = bad_url. - The HTTP call is made with a plain
reqwest::Client(user agentProxyBuilder/<version>), not throughfuentes::Externo(see External-Sources); status 404/403 map toNotFound, other non-2xx toNetwork. -
desde_jsonreads only the fields it needs (categories[].includedInDeck,cards[].quantity/categories/card.uid/collectorNumber/edition.editioncode/oracleCard.name). A card is skipped if its categories are all excluded from the deck, if it is in the Sideboard category andinclude_sideboardis false, or if quantity is 0. Commander and Sideboard categories set the line section. - It produces a text of lines
"{qty} {name} ({set}) {number}", re-parses that text withdecklist::parse(so "what you see is what gets resolved"), then restores the sections it knew (the text itself carries no headers). It also buildsids: HashMap<line_no, uid>with the exact Scryfall id of each printing, butimport_archidektreturns onlytextandlist; the ids are not sent to the front end. The exact printing is preserved anyway because the generated line includes set code and collector number, which the resolver looks up as aSetNumberidentifier. - Because the section headers are not in the returned text, loading the text through the normal path (
cargarLista->parse_decklist) puts everything inMain; sideboard cards imported from Archidekt only end up markedBanquilloif the user's text has aSideboardheader. Inferred fromdesde_json(the text has no headers) andcargarLista(re-parses the text box). - The fixture
src-tauri/src/fixtures/archidekt-1.jsonis a real deck used byel_deck_real_de_archidekt_se_convierte_en_lista.
| Situation | Result |
|---|---|
| Unreadable line |
LineKind::Invalid + LineError; shown in the editor; counted in invalid_count. Does not stop the load. |
cardCount == 0 |
cargarLista() shows "The list has no cards" and does nothing. |
deck_list_save bad path |
AppError { code: InvalidInput, params: { detalle } }. |
| Archidekt URL not recognised / deck private or missing / network |
InvalidInput(motivo=bad_url) / NotFound / Network. |
| Archidekt JSON malformed |
InvalidInput with detalle. |
-
src-tauri/crates/proxy-core/src/decklist/mod.rs(mod tests): formats, marks, sections, comments-as-headers, About block,SB:, invalid lines, line numbering. -
src-tauri/crates/proxy-core/src/decklist/exportar.rs(mod tests): the four formats, merging, marks, different printings. -
src-tauri/src/archidekt.rs(mod tests): URL forms, real deck fixture, broken JSON. -
src/features/decklist/__tests__/acciones.test.ts:cargarListaflow with a mocked Rust side.
- New list syntax: add a regex/branch in
linea()and a case row in the big table test ofmod.rs; keeprawuntouched and return anInvalidwith aLineErrorrather than dropping lines. New error variants need a translation in the front end (useLang().mensaje). - New export target: add a
FormatoListavariant and amatcharm inexportar(); add the menu entry insrc/features/export/menu-exportar.tsx. - New import site: follow
archidekt.rs(pure JSON ->ParsedDecklistfunction, tested against a fixture) and add the network call in a command; if it downloads anything beyond JSON, route it throughfuentes::Externo.
-
Scryfall-Integration - what happens to a
ParsedDecklistnext (resolve_decklist). -
Deck-Model -
DeckCard,MarcaCarta,cuentan. - Decklist-Panel - the UI that hosts the editor and the Archidekt field.
- Export - PDF export and the "Export PDF" split button that hosts the list menu.
- IPC-and-Error-Handling
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia