/** * Extracts dungeon data from index.original.html and emits TypeScript data files. * Run with: node scripts/extract-data.mjs */ import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { JSDOM } from 'jsdom'; const html = readFileSync('index.original.html', 'utf-8'); const dom = new JSDOM(html); const doc = dom.window.document; // ── Sidebar: extract dungeon list ── const tabs = doc.querySelectorAll('.dungeon-tab'); const dungeonList = []; for (const tab of tabs) { const img = tab.querySelector('img'); const href = tab.getAttribute('href'); const isDisabled = tab.classList.contains('disabled'); const name = tab.textContent.trim(); const icon = img ? `/assets/${img.getAttribute('src').split('/').pop()}` : ''; const id = href ? href.replace('#', '') : name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); dungeonList.push({ id, name, icon, disabled: isDisabled || undefined }); } // ── Parse each dungeon page ── const dungeonPages = doc.querySelectorAll('.dungeon-page'); const dungeonDataMap = {}; for (const page of dungeonPages) { const id = page.getAttribute('id'); // Header const header = page.querySelector('.dungeon-header'); const headerImg = header?.querySelector('img'); const headerText = header?.querySelector('.dungeon-header-text'); const name = headerText?.querySelector('h1')?.textContent?.trim() || id; const descP = headerText?.querySelector('p'); const descriptionHtml = descP ? descP.innerHTML.trim() : ''; const headerImage = headerImg ? `/assets/${headerImg.getAttribute('src').split('/').pop()}` : ''; // Find icon from dungeonList const listItem = dungeonList.find(d => d.id === id); const icon = listItem?.icon || ''; // Parse sections in order const sections = []; const children = page.children; for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.classList.contains('dungeon-header')) continue; if (child.classList.contains('section-divider')) { sections.push({ type: 'divider' }); continue; } if (child.classList.contains('trash-section')) { sections.push({ type: 'trash', data: parseTrashSection(child) }); continue; } if (child.classList.contains('boss-section')) { sections.push({ type: 'boss', data: parseBossSection(child) }); continue; } } dungeonDataMap[id] = { id, name, descriptionHtml, headerImage, icon, sections }; } function parseTrashSection(el) { const headerEl = el.querySelector('.trash-header'); const header = headerEl?.textContent?.trim() || ''; const content = []; const children = el.children; for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.classList.contains('trash-header')) continue; if (child.classList.contains('quick-ref')) { content.push({ type: 'quickRef', data: { html: child.innerHTML.trim() } }); continue; } if (child.classList.contains('mob-card')) { content.push({ type: 'mob', data: parseMobCard(child) }); continue; } } return { header, content }; } function parseMobCard(el) { const nameEl = el.querySelector('.mob-name'); const name = nameEl?.textContent?.trim() || ''; const nameHtml = nameEl?.innerHTML?.trim() || ''; const tankRelevant = el.classList.contains('tank-relevant'); const abilities = []; const lis = el.querySelectorAll('.mob-abilities li'); for (const li of lis) { abilities.push(li.innerHTML.trim()); } return { name, nameHtml, tankRelevant, abilitiesHtml: abilities }; } function parseBossSection(el) { const headerEl = el.querySelector('.boss-header'); const img = headerEl?.querySelector('img'); const textEl = headerEl?.querySelector('.boss-header-text'); const bossNumber = textEl?.querySelector('.boss-number')?.textContent?.trim() || ''; const bossName = textEl?.querySelector('h2')?.textContent?.trim() || ''; const subtitle = textEl?.querySelector('.boss-subtitle')?.textContent?.trim() || ''; const image = img ? `/assets/${img.getAttribute('src').split('/').pop()}` : ''; const abilities = []; const cards = el.querySelectorAll('.ability-card'); for (const card of cards) { abilities.push(parseAbilityCard(card)); } return { bossNumber, name: bossName, subtitle, image, abilities }; } function parseAbilityCard(el) { const nameEl = el.querySelector('.ability-name'); const link = nameEl?.querySelector('a'); const roleTag = nameEl?.querySelector('.role-tag'); const name = link?.textContent?.trim() || nameEl?.childNodes[0]?.textContent?.trim() || ''; const wowheadUrl = link?.getAttribute('href') || undefined; const roleText = roleTag?.textContent?.trim()?.toLowerCase() || 'everyone'; const role = ['tank', 'healer', 'dps', 'everyone'].includes(roleText) ? roleText : 'everyone'; let importance = null; if (el.classList.contains('tank-important')) importance = 'tank-important'; else if (el.classList.contains('healer-important')) importance = 'healer-important'; else if (el.classList.contains('everyone-important')) importance = 'everyone-important'; const descEl = el.querySelector('.ability-desc'); const descriptionHtml = descEl?.innerHTML?.trim() || ''; return { name, wowheadUrl, role, importance, descriptionHtml }; } // ── Emit TypeScript files ── mkdirSync('src/data', { recursive: true }); function escapeForTemplate(str) { return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${'); } function emitDungeonFile(dungeon) { const slug = dungeon.id; const varName = slug.replace(/-./g, m => m[1].toUpperCase()); let ts = `import type { DungeonData } from "@/types/dungeon";\n\n`; ts += `export const ${varName}: DungeonData = {\n`; ts += ` id: ${JSON.stringify(dungeon.id)},\n`; ts += ` name: ${JSON.stringify(dungeon.name)},\n`; ts += ` descriptionHtml: \`${escapeForTemplate(dungeon.descriptionHtml)}\`,\n`; ts += ` headerImage: ${JSON.stringify(dungeon.headerImage)},\n`; ts += ` icon: ${JSON.stringify(dungeon.icon)},\n`; ts += ` sections: [\n`; for (const section of dungeon.sections) { if (section.type === 'divider') { ts += ` { type: "divider" },\n`; } else if (section.type === 'trash') { ts += emitTrashSection(section.data); } else if (section.type === 'boss') { ts += emitBossSection(section.data); } } ts += ` ],\n`; ts += `};\n`; writeFileSync(`src/data/${slug}.ts`, ts); console.log(` Wrote src/data/${slug}.ts`); return { slug, varName }; } function emitTrashSection(data) { let ts = ` {\n type: "trash",\n data: {\n`; ts += ` header: ${JSON.stringify(data.header)},\n`; ts += ` content: [\n`; for (const item of data.content) { if (item.type === 'quickRef') { ts += ` { type: "quickRef", data: { html: \`${escapeForTemplate(item.data.html)}\` } },\n`; } else if (item.type === 'mob') { ts += emitMobCard(item.data); } } ts += ` ],\n`; ts += ` },\n },\n`; return ts; } function emitMobCard(data) { let ts = ` {\n type: "mob",\n data: {\n`; ts += ` name: ${JSON.stringify(data.name)},\n`; ts += ` nameHtml: \`${escapeForTemplate(data.nameHtml)}\`,\n`; ts += ` tankRelevant: ${data.tankRelevant},\n`; ts += ` abilitiesHtml: [\n`; for (const ab of data.abilitiesHtml) { ts += ` \`${escapeForTemplate(ab)}\`,\n`; } ts += ` ],\n`; ts += ` },\n },\n`; return ts; } function emitBossSection(data) { let ts = ` {\n type: "boss",\n data: {\n`; ts += ` bossNumber: ${JSON.stringify(data.bossNumber)},\n`; ts += ` name: ${JSON.stringify(data.name)},\n`; ts += ` subtitle: ${JSON.stringify(data.subtitle)},\n`; ts += ` image: ${JSON.stringify(data.image)},\n`; ts += ` abilities: [\n`; for (const ab of data.abilities) { ts += ` {\n`; ts += ` name: ${JSON.stringify(ab.name)},\n`; if (ab.wowheadUrl) { ts += ` wowheadUrl: ${JSON.stringify(ab.wowheadUrl)},\n`; } ts += ` role: ${JSON.stringify(ab.role)},\n`; ts += ` importance: ${JSON.stringify(ab.importance)},\n`; ts += ` descriptionHtml: \`${escapeForTemplate(ab.descriptionHtml)}\`,\n`; ts += ` },\n`; } ts += ` ],\n`; ts += ` },\n },\n`; return ts; } // Emit individual dungeon files console.log('Extracting dungeon data...'); const exports = []; for (const [id, dungeon] of Object.entries(dungeonDataMap)) { exports.push(emitDungeonFile(dungeon)); } // Emit index.ts let indexTs = `import type { DungeonData, DungeonListItem } from "@/types/dungeon";\n`; for (const { slug, varName } of exports) { indexTs += `import { ${varName} } from "./${slug}";\n`; } indexTs += `\nexport const dungeonList: DungeonListItem[] = [\n`; for (const item of dungeonList) { indexTs += ` { id: ${JSON.stringify(item.id)}, name: ${JSON.stringify(item.name)}, icon: ${JSON.stringify(item.icon)}`; if (item.disabled) indexTs += `, disabled: true`; indexTs += ` },\n`; } indexTs += `];\n`; indexTs += `\nexport const dungeonDataMap: Record = {\n`; for (const { slug, varName } of exports) { indexTs += ` "${slug}": ${varName},\n`; } indexTs += `};\n`; writeFileSync('src/data/index.ts', indexTs); console.log(' Wrote src/data/index.ts'); console.log(`\nDone! Extracted ${exports.length} dungeons.`); // Summary for (const [id, dungeon] of Object.entries(dungeonDataMap)) { const bosses = dungeon.sections.filter(s => s.type === 'boss').length; const trash = dungeon.sections.filter(s => s.type === 'trash').length; console.log(` ${dungeon.name}: ${bosses} bosses, ${trash} trash sections`); }