store: el árbol de categorías, igual que el original (colores y sin fantasmas)
Investigadas las 750 categorías de la BD contra el HTML real del original. Los
ítems estaban perfectos (554 hojas, 3068 ítems, mismo contenido en cada una: no
faltaba ninguno). Todo lo roto era de categorías:
- 45 CATEGORÍAS FANTASMA, todas con nombre de código ("11-1", "23-10"). El seed
dedujo el árbol de los códigos de las hojas ("11-1-1" => padre "11-1"), pero
el original NO siempre pinta ese nivel: en 45 hojas el "-1-" del código no
corresponde a nada y cuelgan directas de su raíz. Ahora se lee el anidamiento
real (<ul>/<li>): 705 categorías (20 + 131 + 554) en vez de 750.
- El nombre de una categoría NO es texto plano, es HTML, y lo estábamos
aplanando: 141 nombres perdían su color y 40 cabeceras el suyo. Hay dos
mecanismos y los dos hacen falta: la clase en la CABECERA, que sustituye a
first-brown para colorear la categoría entera ("Brujo" en color de brujo), y
los <span class="no-toggle X"> dentro del nombre (clases y las etiquetas
[Nivel 232 a 245] / (DPS) en otro tono).
- 21 nombres TRUNCADOS en Tokens de Armadura: guardábamos "Guerrero" donde el
original dice "Guerrero, Sacerdote, Druida" (solo se quedó el primer span).
El HTML se trocea en el SERVIDOR (`parseCategoryName`) y al cliente le llegan
solo texto + nombre de clase, así que no hay HTML de la BD entrando en el DOM.
El texto sin color va suelto, NO envuelto en <span>: el tema tiene
`span { color: #fff }` y lo pondría blanco en vez de heredar el de la cabecera.
`name` pasa a VARCHAR(512): con el HTML, el más largo mide 198 y no cabía en 191.
El name_en se genera en el propio seed traduciendo solo el TEXTO y respetando
las etiquetas, así que sql/store_category_en.sql sobra y se borra.
gen_store_catalog.py regenera el seed entero (categorías del HTML, ítems de la
BD, que ya estaban verificados) y es reproducible byte a byte.
Verificado en el navegador contra el original: mismo árbol, 0 categorías con
nombre de código, "[Nivel 232 a 245]" en marrón tenue y los colores de clase
oficiales (Guerrero #C79C6E, Sacerdote blanco, Druida #FF7D0A). En inglés
también, conservando los colores.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+61
-3
@@ -27,9 +27,25 @@ export interface StoreItem {
|
||||
preview: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Un trozo del nombre de una categoría: texto y, si lo lleva, la clase de color
|
||||
* con la que hay que pintarlo (`warrior`, `second-brown`…).
|
||||
*
|
||||
* El nombre viene de BD como HTML (así lo hace el original: colorea los nombres
|
||||
* de clase y pone las etiquetas en otro tono). Se trocea AQUÍ, en el servidor, y
|
||||
* al cliente le llega ya estructurado: así se pinta con React y no hace falta
|
||||
* meter HTML de la BD en el DOM.
|
||||
*/
|
||||
export interface NamePart {
|
||||
text: string
|
||||
cls?: string
|
||||
}
|
||||
|
||||
export interface StoreCategory {
|
||||
code: string
|
||||
name: string
|
||||
name: NamePart[]
|
||||
/** Clase de la cabecera; a veces sustituye a first-brown para colorearla entera. */
|
||||
css: string
|
||||
depth: number
|
||||
children: StoreCategory[]
|
||||
items: StoreItem[]
|
||||
@@ -39,6 +55,7 @@ interface CatRow extends RowDataPacket {
|
||||
code: string
|
||||
parent_code: string | null
|
||||
name: string
|
||||
css: string
|
||||
depth: number
|
||||
sort: number
|
||||
}
|
||||
@@ -55,6 +72,40 @@ interface ItemRow extends RowDataPacket {
|
||||
sort: number
|
||||
}
|
||||
|
||||
/** `<span class="no-toggle X">…</span>` — la única etiqueta que usa el original. */
|
||||
const NAME_SPAN = /<span class="no-toggle ([a-z0-9- ]+)">([^<]*)<\/span>/g
|
||||
|
||||
function unescapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, '&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Trocea el nombre HTML de una categoría en partes con su color.
|
||||
*
|
||||
* Solo entiende `<span class="no-toggle X">`, que es lo único que usa el
|
||||
* catálogo. Cualquier otra etiqueta se ignora y queda su texto: si alguien mete
|
||||
* HTML raro en la BD, aquí NO puede acabar inyectado (al cliente solo le llegan
|
||||
* texto y un nombre de clase).
|
||||
*/
|
||||
export function parseCategoryName(html: string): NamePart[] {
|
||||
const parts: NamePart[] = []
|
||||
let last = 0
|
||||
for (const m of html.matchAll(NAME_SPAN)) {
|
||||
const plain = html.slice(last, m.index).replace(/<[^>]*>/g, '')
|
||||
if (plain) parts.push({ text: unescapeHtml(plain) })
|
||||
if (m[2]) parts.push({ text: unescapeHtml(m[2]), cls: m[1] })
|
||||
last = m.index + m[0].length
|
||||
}
|
||||
const tail = html.slice(last).replace(/<[^>]*>/g, '')
|
||||
if (tail) parts.push({ text: unescapeHtml(tail) })
|
||||
return parts.length ? parts : [{ text: unescapeHtml(html.replace(/<[^>]*>/g, '')) }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Árbol completo de la tienda (categorías anidadas con sus ítems en las hojas).
|
||||
*
|
||||
@@ -75,7 +126,7 @@ export async function getStoreCatalog(locale: string): Promise<StoreCategory[]>
|
||||
let items: ItemRow[] = []
|
||||
try {
|
||||
;[cats] = await db(DB.default).query<CatRow[]>(
|
||||
`SELECT code, parent_code, COALESCE(NULLIF(${catNameCol}, ''), name) AS name, depth, sort
|
||||
`SELECT code, parent_code, COALESCE(NULLIF(${catNameCol}, ''), name) AS name, css, depth, sort
|
||||
FROM home_store_category ORDER BY sort`,
|
||||
)
|
||||
;[items] = await db(DB.default).query<ItemRow[]>(
|
||||
@@ -91,7 +142,14 @@ export async function getStoreCatalog(locale: string): Promise<StoreCategory[]>
|
||||
|
||||
const nodes = new Map<string, StoreCategory>()
|
||||
for (const c of cats) {
|
||||
nodes.set(c.code, { code: c.code, name: c.name, depth: c.depth, children: [], items: [] })
|
||||
nodes.set(c.code, {
|
||||
code: c.code,
|
||||
name: parseCategoryName(c.name),
|
||||
css: c.css || 'first-brown',
|
||||
depth: c.depth,
|
||||
children: [],
|
||||
items: [],
|
||||
})
|
||||
}
|
||||
const roots: StoreCategory[] = []
|
||||
for (const c of cats) {
|
||||
|
||||
Reference in New Issue
Block a user