'
r'|'
)
SPAN_TAG = re.compile(r'?span\b')
def span_inner(s, start):
"""Contenido de un , respetando anidamiento."""
i = s.index('>', start) + 1
depth, j = 1, i
while True:
m = SPAN_TAG.search(s, j)
if not m:
return s[i:]
if m.group(0) == '\s*', '', name)
return re.sub(r'\s+', ' ', name).strip()
def parse_tree(html):
"""[(code, parent, name_html, css, depth)] en orden de documento."""
out, seen = [], set()
root = sub = pending = None
for m in TOKEN.finditer(html):
kind, cls, code = m.group(1), m.group(2), m.group(3)
if kind:
entry = (clean(span_inner(html, m.start())), cls)
if kind == 'cat':
root, sub, pending = entry, None, None
elif kind == 'subcat':
sub, pending = entry, None
else:
pending = entry
elif code and pending:
p = code.split('-')
if len(p) != 3:
continue
rc, sc = p[0], f'{p[0]}-{p[1]}'
if root and rc not in seen:
seen.add(rc); out.append((rc, None, root[0], root[1], 1))
# La subcategoría solo existe si el HTML la pinta de verdad.
if sub and sc not in seen:
seen.add(sc); out.append((sc, rc, sub[0], sub[1], 2))
if code not in seen:
seen.add(code)
out.append((code, sc if sub else rc, pending[0], pending[1], 3))
pending = None
return out
# Traducción respetando el HTML. Solo hay dos formas en los datos reales:
# A) "TEXTO (SUFIJO)"
# B) "Clase, ..."
TAG_A = re.compile(r'^(.*?)\s*([\(\[].*?[\)\]])$')
SPAN_B = re.compile(r'([^<]+)')
SUFIJO = re.compile(r'\s*([\(\[][^\)\]]*[\)\]])\s*$')
def tr_html(name):
"""Traduce el nombre conservando las etiquetas. None si no sabe."""
if '<' not in name:
return tr(name)
m = TAG_A.match(name)
if m:
# Se traduce la frase ENTERA (el traductor tiene reglas de frase: "Nivel
# de objeto 232 (DPS)"), y luego se vuelve a envolver el sufijo.
base, cls, _tag = m.groups()
full = tr(f'{base} {_tag}'.strip())
if not full:
return None
s = SUFIJO.search(full)
if not s:
return full
return f'{full[:s.start()].strip()} {s.group(1)}'
if name.startswith('{t or mm.group(2)}'
return SPAN_B.sub(one, name)
return None
def esc(s):
return s.replace('\\', '\\\\').replace("'", "''")
def db_items():
"""Ítems desde la BD (ya verificados contra el original)."""
env = os.path.join(HERE, '..', '..', '.env.local')
cfg = dict(re.findall(r'^(\w+)=(.*)$', open(env, encoding='utf-8').read(), re.M))
q = ('SELECT category_code,item_id,name,IFNULL(icon,""),quantity,currency,price,'
'IFNULL(preview,""),sort FROM home_store_item ORDER BY sort,id')
out = subprocess.run(
['mysql', '-h', cfg.get('DB_HOST', '127.0.0.1'), '-u', cfg['DB_USER'],
f'-p{cfg["DB_PASSWORD"]}', '--default-character-set=utf8mb4', '-N', '--raw',
cfg.get('DB_NAME_DEFAULT', 'django_wow'), '-e', q],
capture_output=True, text=True, check=True)
return [l.split('\t') for l in out.stdout.splitlines() if l]
cats = parse_tree(open(SRC, encoding='utf-8', errors='replace').read())
items = db_items()
sin_tr = [c for c in cats if tr_html(c[2]) is None]
print(f'categorías: {len(cats)} | ítems: {len(items)} | sin traducir: {len(sin_tr)}', file=sys.stderr)
for c in sin_tr[:10]:
print(' sin traducir:', c[0], c[2][:60], file=sys.stderr)
with open(OUT, 'w', encoding='utf-8') as f:
f.write("""-- Catálogo de la tienda (categorías + ítems), extraído del sistema antiguo (BENNU).
-- NO editar a mano: regenerar con `sql/db2/gen_store_catalog.py` (ver su cabecera).
--
-- El nombre de una categoría es HTML a propósito: el original colorea los
-- nombres de clase y pone las etiquetas ([Nivel 232 a 245], (DPS)) en otro tono.
-- `css` es la clase de la CABECERA, que a veces sustituye a first-brown para
-- colorear la categoría entera (p.ej. "Brujo" en color de brujo).
SET NAMES utf8mb4;
DROP TABLE IF EXISTS home_store_item;
DROP TABLE IF EXISTS home_store_category;
CREATE TABLE home_store_category (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(32) NOT NULL UNIQUE,
parent_code VARCHAR(32) NULL,
-- 512 y no 191: los nombres llevan HTML y el más largo pasa de 191.
name VARCHAR(512) NOT NULL COMMENT 'HTML: puede llevar ',
name_en VARCHAR(512) DEFAULT NULL COMMENT 'igual que name, en inglés; NULL = usar name',
css VARCHAR(32) NOT NULL DEFAULT 'first-brown' COMMENT 'clase de la cabecera',
depth TINYINT NOT NULL,
sort INT NOT NULL,
KEY k_parent (parent_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE home_store_item (
id INT AUTO_INCREMENT PRIMARY KEY,
category_code VARCHAR(32) NOT NULL,
item_id INT NOT NULL,
name VARCHAR(191) NOT NULL,
icon VARCHAR(128) NULL,
quantity INT NOT NULL DEFAULT 1,
currency ENUM('pd','pv') NOT NULL DEFAULT 'pd',
price INT NOT NULL,
preview VARCHAR(64) NULL,
sort INT NOT NULL,
KEY k_cat (category_code),
KEY k_item (item_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")
CH = 200
for i in range(0, len(cats), CH):
f.write('INSERT INTO home_store_category (code,parent_code,name,name_en,css,depth,sort) VALUES\n')
rows = []
for n, (code, parent, name, css, depth) in enumerate(cats[i:i + CH], start=i):
en = tr_html(name)
rows.append("('%s',%s,'%s',%s,'%s',%d,%d)" % (
esc(code), f"'{esc(parent)}'" if parent else 'NULL', esc(name),
f"'{esc(en)}'" if en else 'NULL', esc(css), depth, n))
f.write(',\n'.join(rows) + ';\n')
for i in range(0, len(items), CH):
f.write('INSERT INTO home_store_item (category_code,item_id,name,icon,quantity,currency,price,preview,sort) VALUES\n')
rows = []
for cc, iid, nm, ic, qty, cur, pr, pv, so in items[i:i + CH]:
rows.append("('%s',%s,'%s',%s,%s,'%s',%s,%s,%s)" % (
esc(cc), iid, esc(nm), f"'{esc(ic)}'" if ic else 'NULL', qty, cur, pr,
f"'{esc(pv)}'" if pv else 'NULL', so))
f.write(',\n'.join(rows) + ';\n')
print(f'escrito {OUT}', file=sys.stderr)