initial commit
This commit is contained in:
283
scripts/extract-data.mjs
Normal file
283
scripts/extract-data.mjs
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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<string, DungeonData> = {\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`);
|
||||
}
|
||||
Reference in New Issue
Block a user