Repository navigation
Parseo de la lista del mazo
English · Español
Proxy Builder convierte el texto que pega el usuario (o una URL de Archidekt) en una lista estructurada de líneas y, más tarde, convierte el mazo terminado de nuevo en texto para Moxfield, Arena, MTGO o texto plano. Ambos sentidos viven en el crate puro proxy-core (src-tauri/crates/proxy-core/src/decklist/), así que no usan red, ni Tauri, ni base de datos; la capa de Tauri solo los envuelve en comandos.
Límites: esta página cubre leer y escribir el texto de la lista. Buscar las cartas (Scryfall, idiomas, imágenes) es Integración con Scryfall; lo que ocurre con las cartas resultantes dentro del mazo es Modelo del mazo.
| Fichero | Responsabilidad |
|---|---|
src-tauri/crates/proxy-core/src/decklist/mod.rs |
parse(), ParsedDecklist, ParsedLine, Section, LineKind, LineError; la gramática. |
src-tauri/crates/proxy-core/src/decklist/exportar.rs |
exportar() y FormatoLista: mazo -> texto para Moxfield / Arena / MTGO / texto plano. |
src-tauri/src/commands/decklist.rs |
Comandos de Tauri parse_decklist, deck_list_text, deck_list_save. |
src-tauri/src/archidekt.rs |
id_de_url(), desde_json(): importación de un mazo público de Archidekt por URL. |
src-tauri/src/commands/cards.rs |
Comando import_archidekt (hace la llamada HTTP y luego llama a archidekt::desde_json). |
src/features/decklist/editor-lista.tsx |
El cuadro de texto de la lista: llama a parse_decklist 250 ms después de la última pulsación y marca las líneas no válidas. |
src/features/decklist/acciones.ts |
cargarLista(): parse_decklist y luego resolve_decklist. |
src/features/export/menu-exportar.tsx |
El menú «...» junto a «Exportar PDF»: copiar/guardar la lista. |
El parser nunca adivina en silencio: cada línea de entrada vuelve como una ParsedLine con un kind, y una línea que no se puede entender lleva un código LineError para que la interfaz pueda marcarla. La interfaz traduce el código; Rust nunca escribe prosa (véase IPC y manejo de errores).
Todos los tipos derivan TS con #[ts(export)] y pnpm bindings los genera en src/bindings/.
ParsedDecklist:
| Campo | Significado |
|---|---|
lines: Vec<ParsedLine> |
Una entrada por línea de entrada (incluidas las líneas en blanco y los comentarios), en orden. |
card_count: u32 |
Suma de qty de las líneas que son Card e included. |
invalid_count: u32 |
Número de líneas Invalid. |
ParsedLine:
| Campo | Significado |
|---|---|
line_no |
Número de línea, desde 1. |
raw |
El texto original, intacto. |
kind |
LineKind: Card, Section, Comment, Empty, Invalid. |
qty, name
|
Copias y nombre limpio de la carta (solo tiene sentido para Card). |
set, collector
|
Código de set en minúsculas (cmm) y número de coleccionista, si la línea los traía. |
foil |
true si la línea llevaba una marca de foil/etched/comandante (véase «Marcas»). |
section |
Section: Main, Commander, Sideboard, Companion, Maybeboard, Tokens. |
included |
false para el Maybeboard y para las categorías de Archidekt marcadas {noDeck}. Solo se resuelven las líneas incluidas. |
error |
LineError: ZeroQuantity, BadQuantity, TooMany (> 999 copias, MAX_COPIAS), EmptyName. |
Section::included_by_default() es true para todo salvo Maybeboard.
La cabecera del módulo documenta la intención: aceptar lo que acepta el sitio de referencia, más las exportaciones de los sitios habituales. En concreto (todo cubierto por formatos_de_arena_moxfield_y_archidekt y sus vecinas en los tests de mod.rs):
| Origen | Línea de ejemplo |
|---|---|
| Texto plano |
Sol Ring, 2x Sol Ring, 3 x Island, 2X Counterspell
|
| Con impresión |
1 Sol Ring (CMM) 410, Bird (teoc) (solo set), 1 Llanowar Elves (PLST) DOM-168, 1 Sol Ring (SLD) 2063★
|
| MTG Arena |
4 Lightning Bolt (M10) 146, bloque de cabecera About / Name My deck, cabeceras Deck / Sideboard / Commander, prefijo antiguo SB: 2 Duress
|
| Moxfield |
1 Fire // Ice (MH2) 290 *F*, *E*, *CMDR*, cabeceras con recuentos como Commander (1)
|
| Archidekt | 1x Sol Ring (cmm) 410 [Commander{top}] ^Have,#37d67a^ |
| Doble cara / partidas |
1 Delver of Secrets // Insectile Aberration (ISD) 51, 1 Who // What // When // Where // Why
|
parse(texto) recorre texto.lines() una sola vez, arrastrando dos datos de estado entre líneas: la Section actual (empieza en Main) y un indicador en_about (dentro de un bloque «About» de Arena). Cada línea pasa por linea():
-
Línea en blanco:
kind = Empty, reiniciaen_about. No cambia la sección. -
Comentario o cabecera: si la línea recortada empieza por
//o#, se quitan esos caracteres y lo que queda se prueba como cabecera; si no, se prueba la línea entera. Si es una cabecera (seccion_de_cabecera), la línea pasa a serSectiony cambia la sección actual; si empezaba por///#pero no es una cabecera, es unCommenty la sección no cambia.- La comparación de cabeceras pasa a minúsculas, recorta un
:final y descarta un recuento(n)final (Commander (1),Sideboard (15)). - Palabras reconocidas:
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. - Por qué también se aceptan cabeceras como comentarios: algunos sitios pegan la cabecera como comentario al copiar una lista como texto (
#SIDEBOARD,// Sideboard (15)); un test (cabeceras_escritas_como_comentario) lo fija.
- La comparación de cabeceras pasa a minúsculas, recorta un
-
Bloque About de Arena: tras una cabecera
About, una línea que empiece porNamees unComment(el nombre del mazo, no una carta) hasta la siguiente línea en blanco. -
Prefijo
SB::SB:(sin distinguir mayúsculas) pone solo esa línea enSideboardsin cambiar la sección en curso. -
Sufijos de Archidekt (
quitar_archidekt): se descarta un bloque de etiquetas^...^al final y luego se lee una categoría[...]al final. Una categoría que contenga{nodeck}o empiece pormaybeboardponeincluded = false; si empieza pormaybeboardademás pone la sección enMaybeboard; si empieza porcommanderosideboardfija esa sección para la línea. -
Marcas:
*F*,*E*,*CMDR*,*Foil*,*Etched*(sin distinguir mayúsculas, en cualquier posición) se eliminan y ponenfoil = true. Rareza:*CMDR*también ponefoil(la misma regex las trata todas); por ahora nada posterior leeParsedLine.foil(comprobado: ningún consumidor fuera dedecklist/mod.rs). -
Cantidad (
RE_CANTIDAD: dígitos,x/Xopcional, luego espacio en blanco y el resto). Sin número inicial, la cantidad es 1. Una línea que parece una cantidad errónea (RE_CANTIDAD_MAL:x3 ...,3xx ..., un4o4xsolos) pasa a serInvalidconBadQuantity, oEmptyNamecuando solo son dígitos/x. Cantidad 0 ->ZeroQuantity; por encima de 999 ->TooMany. -
Set y número (
RE_SET):name (SET) [number], donde SET son 2-8 caracteres alfanuméricos entre paréntesis. El número es el siguiente token que no es un espacio y puede ser cualquier cosa (410,2063★,DOM-168,254p,38a). Los paréntesis con espacios dentro (por ejemplo, un nombre de carta con un paréntesis) no coinciden y siguen formando parte del nombre. -
Limpieza del nombre (
limpiar_nombre): las comillas tipográficas pasan a ASCII (',") y las secuencias de espacios en blanco se colapsan. Un nombre vacío esInvalid/EmptyName. - La línea que sobrevive es una
Cardconqty,name,set,collector,sectioneincluded(a partir del valor por defecto de la sección, posiblemente modificado en el paso 5).
- Cualquier línea entera igual a una palabra de cabecera es una cabecera, incluso sin
//ni#. Una carta que se llame literalmente como una palabra de cabecera, sola en su línea, sería interpretada como un cambio de sección. -
Deducido de la regex: una carta cuyo nombre empieza por un número y un espacio (p. ej.
1996 World Champion) se lee como cantidad 1996 y se rechaza conTooMany; los nombres al estilo7 Wonderspierden su número. Escribe1 1996 World Championen su lugar. -
1Island(sin espacio) no es una cantidad: se lee como una carta llamada1Islandcon cantidad 1. - Los comentarios nunca cambian la sección; las líneas en blanco tampoco (solo un bloque
Abouttermina en una línea en blanco). - Los finales de línea de Windows los gestiona
str::lines();rawconserva el texto de la línea sin el terminador. - El parser es deliberadamente permisivo con la existencia de las cartas: solo comprueba la sintaxis. Un nombre mal escrito se parsea bien y falla después como
Unresolveden el resolvedor.
La Section de una línea es el único sitio donde, en el momento del parseo, se sabe que «esta carta pertenece al banquillo». resolve.rs la copia a las cartas creadas como una marca (DeckCard.marca, tipo MarcaCarta en src-tauri/crates/proxy-core/src/model.rs):
| Sección de la línea | Marca en las copias resueltas | Efecto |
|---|---|---|
Sideboard |
MarcaCarta::Banquillo |
Se imprime, pero queda fuera del tamaño del mazo, de las copias y de las estadísticas; se exporta en el banquillo. |
Maybeboard |
MarcaCarta::Opcional |
Se imprime, queda fuera de los recuentos. |
| cualquier otra | ninguna | Cuenta con normalidad. |
proxy_core::model::cuentan() es la única función que decide qué cuenta (también gestiona Variante; una variante de diseño cuenta solo si no existe una copia sin marcar del mismo nombre). Detalles: Modelo del mazo y Estadísticas.
Nota: resolve() solo procesa las líneas con kind == Card && included. A través de parse(), una línea del Maybeboard tiene included = false, así que la rama Opcional de resolve.rs solo se alcanza si quien llama pone included = true en una línea así; la interfaz no tiene hoy ningún interruptor para eso (ningún código del frontend lee included). El importador de Archidekt descarta por la misma razón todo lo que el sitio marca fuera del mazo.
exportar(deck: &Deck, formato: FormatoLista) -> String construye el texto a partir de las cartas del mazo, no del texto pegado. FormatoLista: Moxfield, Arena, Mtgo, Texto.
Cómo se construye un conjunto de líneas (juntar):
- Las secciones no se guardan en las cartas. Se recuperan volviendo a parsear
deck.decklist_text(secciones_de) y buscando cada carta por su nombre en minúsculas; una carta de doble cara también coincide por su cara frontal (seccion_de). Una carta ausente del texto pasa por defecto aMain. - Las cartas se filtran por las marcas:
cuentan(&deck.cards)elimina las copias opcionales y las variantes sobrantes; las cartas marcadasBanquillose fuerzan aSideboardindependientemente del texto. - Se omiten las fichas, los huecos, las secciones
Maybeboard/Tokensy las cartas sin nombre (va_en_la_lista). Una cartaCustomvinculada a una fuente externa (ficha de Cockatrice) es una ficha; las demás cartas propias solo se omiten si su línea de tipo dice ficha/emblema (token/emblem). - Las copias de la misma impresión en la misma sección se fusionan en una línea con un recuento (coincidencia por sección + nombre + set + número de coleccionista). Las impresiones distintas quedan en líneas distintas.
- Para las cartas de doble cara,
delantees el nombre de la cara frontal (lo que quieren Arena y MTGO); Moxfield y el texto plano conservan el nombre completoA // B.
Salida por formato (fijada por los tests para_moxfield, para_arena, para_mtgo_el_comandante_va_en_el_banquillo, en_texto_plano):
| Formato | Disposición |
|---|---|
Moxfield |
Comandante + compañero + principal como un solo bloque de N Name (SET) number, luego una línea en blanco, SIDEBOARD: y el banquillo. |
Arena |
Bloques con cabecera Commander, Companion, Deck, Sideboard (los bloques vacíos se omiten), nombres de la cara frontal, (SET) number. |
Mtgo |
N Name con el mazo principal primero, luego una línea en blanco y después comandante/compañero/banquillo juntos (MTGO no tiene casilla de comandante). |
Texto |
N Name, comandante + principal juntos, una línea en blanco y luego el banquillo. |
Los códigos de set se pasan a mayúsculas al exportar. Un set sin número imprime solo (SET).
| Comando | Firma | Notas |
|---|---|---|
parse_decklist |
(text: String) -> ParsedDecklist |
Síncrono y barato; se llama mientras se escribe (con espera de 250 ms en editor-lista.tsx) y de nuevo desde cargarLista(). |
deck_list_text |
(deck: Deck, format: FormatoLista) -> String |
Lo usan las entradas de «copiar la lista» de menu-exportar.tsx. |
deck_list_save |
(deck, format, path) -> () |
La ruta debe ser absoluta y terminar en .txt (si no, InvalidInput); se escribe con escribir_atomico. La interfaz lo usa para «Descargar para MTGO (.txt)». |
import_archidekt |
(url: String, include_sideboard: bool) -> ArchidektImport { text, list } |
Véase más abajo. |
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: id del mazo (o 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: poner el texto en el cuadro de la lista (sin entrada en el historial)
-
id_de_urlaceptaarchidekt.com/decks/<id>/..., conwww., con o sin esquema, y.../api/decks/<id>/. Cualquier otra cosa devuelveNone, que el comando notifica comoInvalidInputconmotivo = bad_url. - La llamada HTTP se hace con un
reqwest::Clientnormal (user agentProxyBuilder/<version>), no a través defuentes::Externo(véase Fuentes externas); los estados 404/403 se convierten enNotFoundy cualquier otro no 2xx enNetwork. -
desde_jsonlee solo los campos que necesita (categories[].includedInDeck,cards[].quantity/categories/card.uid/collectorNumber/edition.editioncode/oracleCard.name). Una carta se omite si todas sus categorías están excluidas del mazo, si está en la categoría Sideboard einclude_sideboardes falso, o si la cantidad es 0. Las categorías Commander y Sideboard fijan la sección de la línea. - Produce un texto de líneas
"{qty} {name} ({set}) {number}", lo vuelve a parsear condecklist::parse(para que «lo que ves es lo que se resuelve») y luego restaura las secciones que conocía (el texto en sí no lleva cabeceras). También construyeids: HashMap<line_no, uid>con el id exacto de Scryfall de cada impresión, peroimport_archidektdevuelve solotextylist; los ids no se envían al frontend. La impresión exacta se conserva igualmente porque la línea generada incluye el código de set y el número de coleccionista, que el resolvedor busca como un identificadorSetNumber. - Como las cabeceras de sección no están en el texto devuelto, cargar el texto por la vía normal (
cargarLista->parse_decklist) pone todo enMain; las cartas del banquillo importadas de Archidekt solo acaban marcadas comoBanquillosi el texto del usuario tiene una cabeceraSideboard. Deducido dedesde_json(el texto no tiene cabeceras) y decargarLista(vuelve a parsear el cuadro de texto). - El fixture
src-tauri/src/fixtures/archidekt-1.jsones un mazo real que usael_deck_real_de_archidekt_se_convierte_en_lista.
| Situación | Resultado |
|---|---|
| Línea ilegible |
LineKind::Invalid + LineError; se muestra en el editor; cuenta en invalid_count. No detiene la carga. |
cardCount == 0 |
cargarLista() muestra «La lista no tiene cartas» y no hace nada. |
Ruta incorrecta en deck_list_save
|
AppError { code: InvalidInput, params: { detalle } }. |
| URL de Archidekt no reconocida / mazo privado o inexistente / red |
InvalidInput(motivo=bad_url) / NotFound / Network. |
| JSON de Archidekt mal formado |
InvalidInput con detalle. |
-
src-tauri/crates/proxy-core/src/decklist/mod.rs(mod tests): formatos, marcas, secciones, comentarios como cabeceras, bloque About,SB:, líneas no válidas, numeración de líneas. -
src-tauri/crates/proxy-core/src/decklist/exportar.rs(mod tests): los cuatro formatos, fusión, marcas, impresiones distintas. -
src-tauri/src/archidekt.rs(mod tests): formas de URL, fixture de un mazo real, JSON roto. -
src/features/decklist/__tests__/acciones.test.ts: el flujo decargarListacon el lado de Rust simulado.
- Nueva sintaxis de lista: añade una regex/rama en
linea()y una fila de caso en el test de tabla grande demod.rs; dejarawintacto y devuelve unInvalidcon unLineErroren lugar de descartar líneas. Las variantes de error nuevas necesitan una traducción en el frontend (useLang().mensaje). - Nuevo destino de exportación: añade una variante de
FormatoListay un brazo dematchenexportar(); añade la entrada de menú ensrc/features/export/menu-exportar.tsx. - Nuevo sitio de importación: sigue
archidekt.rs(una función pura JSON ->ParsedDecklist, probada contra un fixture) y añade la llamada de red en un comando; si descarga algo más que JSON, pásalo porfuentes::Externo.
-
Integración con Scryfall - lo que ocurre a continuación con un
ParsedDecklist(resolve_decklist). -
Modelo del mazo -
DeckCard,MarcaCarta,cuentan. - Panel de la lista del mazo - la interfaz que aloja el editor y el campo de Archidekt.
- Exportación - la exportación a PDF y el botón partido «Exportar PDF» que aloja el menú de la lista.
- IPC y manejo de errores
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