const { JSDOM } = require("jsdom");
const { TypeError, Error } = require("../errors");
const { URLs, EndPoints } = require("./Constants");
/**
* Parses raw HTML to readable objects
*/
class HTMLParser {
constructor(html, type) {
/**
* The HTML to be parsed
* @type {string}
*/
this.html = html;
/**
* The type to parse
* @type {string}
*/
this.type = type;
}
/**
* Parses the HTML to a readable object
* @return {PlayerData|SquadronData|NewsInfo[]|Wiki[]}
*/
parse() {
switch (this.type.toLowerCase()) {
case EndPoints.user:
return this.parseUser();
case EndPoints.clan:
return this.parseClan();
case EndPoints.news:
return this.parseNews();
case EndPoints.wiki:
return this.parseWiki();
}
throw new TypeError("INVALID_TYPE");
}
parseUser() { // eslint-disable-line complexity
const { window: { document } } = new JSDOM(this.html);
const userinfo = document.getElementsByClassName("user-profile__data-list")[0];
const score = document.getElementsByClassName("profile-score__list-col"),
vehicles = score[0],
elite = score[1],
medals = score[2];
if (!userinfo) throw new TypeError("USER_UNAVAILABLE");
/**
* Represents the player's profile
* @typedef {Object} ProfileInfo
* @property {string} image The URL to the player's in-game avatar
* @property {string} nick The player's in-game name
* @property {?string} title The player's title, if he has one
* @property {?string} squadron The player's squadron, if he's in one
* @property {number} level The player's in-game experience level
* @property {string} registered The date when the player registered
* @property {CountryInfo} usa Statistics for the USA
* @property {CountryInfo} ussr Statistics for the USSR
* @property {CountryInfo} britain Statistics for Great Britain
* @property {CountryInfo} germany Statistics for Germany
* @property {CountryInfo} japan Statistics for Japan
* @property {CountryInfo} italy Statistics for Italy
* @property {CountryInfo} france Statistics for France
*/
const profile = {
image: document.getElementsByClassName("user-profile__ava-img")[0].src.split("/").slice(3).join("/"),
nick: userinfo.children[0].innerHTML.trim(),
title: userinfo.children[1].innerHTML.trim(),
squadron: userinfo.children[2].children[0].innerHTML.trim(),
level: userinfo.children[3].innerHTML
.trim()
.split(" ")
.slice(1)
.join(""),
registered: userinfo.children[4].innerHTML.trim().split(" ")[2],
/**
* Represents info about a nation for a player's profile
* @typedef {Object} CountryInfo
* @property {number} vehicles The amount of total vehicles
* @property {number} elite The amount of elite (fully researched) vehicles
* @property {number} medals The amount of medals for the country
*/
usa: {
vehicles: parseInt(vehicles.children[1].innerHTML),
elite: parseInt(elite.children[1].innerHTML),
medals: parseInt(medals.children[1].innerHTML)
},
ussr: {
vehicles: parseInt(vehicles.children[2].innerHTML),
elite: parseInt(elite.children[2].innerHTML),
medals: parseInt(medals.children[2].innerHTML)
},
britain: {
vehicles: parseInt(vehicles.children[3].innerHTML),
elite: parseInt(elite.children[3].innerHTML),
medals: parseInt(medals.children[3].innerHTML)
},
germany: {
vehicles: parseInt(vehicles.children[4].innerHTML),
elite: parseInt(elite.children[4].innerHTML),
medals: parseInt(medals.children[4].innerHTML)
},
japan: {
vehicles: parseInt(vehicles.children[5].innerHTML),
elite: parseInt(elite.children[5].innerHTML),
medals: parseInt(medals.children[5].innerHTML)
},
italy: {
vehicles: parseInt(vehicles.children[6].innerHTML),
elite: parseInt(elite.children[6].innerHTML),
medals: parseInt(medals.children[6].innerHTML)
},
france: {
vehicles: parseInt(vehicles.children[7].innerHTML),
elite: parseInt(elite.children[7].innerHTML),
medals: parseInt(medals.children[7].innerHTML)
}
};
const statscard = document.getElementsByClassName("user-profile__stat profile-stat all-stat")[0].children[0];
/**
* Represents player statistics
* @typedef {Object} ProfileStats
* @property {string} victories The amount of victories
* @property {string} completed The amount of completed battles
* @property {string} ratio The victory/battle ratio
* @property {string} sessions The amount of total sessions
* @property {string} deaths The amount of deaths
* @property {string} lions The amount of Silver Lions earnt
* @property {string} playTime The time played
* @property {number} airTargets The amount of air targets destroyed
* @property {number} groundTargets The amount of ground targets destroyed
* @property {number} navalTargets The amount of naval targets destroyed
*/
const stats = {
/**
* Represents statistics per gamemode
* @typedef {Object} DifficultyInfo
* @property {ProfileStats} arcade The info for the Arcade gamemode
* @property {ProfileStats} realistic The info for the Realistic gamemode
* @property {ProfileStats} simulator The info for the Simulator gamemode
*/
arcade: {
victories: !statscard.children[1].children[1].children.length
? parseInt(statscard.children[1].children[1].innerHTML.trim())
: statscard.children[1].children[1].children[0].innerHTML.trim(),
completed: !statscard.children[1].children[2].children.length
? parseInt(statscard.children[1].children[2].innerHTML.trim())
: statscard.children[1].children[2].children[0].innerHTML.trim(),
ratio: !statscard.children[1].children[3].children.length
? statscard.children[1].children[3].innerHTML.trim()
: statscard.children[1].children[3].children[0].innerHTML.trim(),
deaths: !statscard.children[1].children[4].children.length
? parseInt(statscard.children[1].children[4].innerHTML.trim())
: statscard.children[1].children[4].children[0].innerHTML.trim(),
lions: !statscard.children[1].children[5].children.length
? statscard.children[1].children[5].innerHTML.trim()
: statscard.children[1].children[5].children[0].innerHTML.trim(),
playTime: !statscard.children[1].children[6].children.length
? statscard.children[1].children[6].innerHTML.trim()
: statscard.children[1].children[6].children[0].innerHTML.trim(),
airTargets: !statscard.children[1].children[7].children.length
? parseInt(statscard.children[1].children[7].innerHTML.trim())
: statscard.children[1].children[7].children[0].innerHTML.trim(),
groundTargets: !statscard.children[1].children[8].children.length
? parseInt(statscard.children[1].children[8].innerHTML.trim())
: statscard.children[1].children[8].children[9].innerHTML.trim(),
navalTargets: !statscard.children[1].children[1].children.length
? parseInt(statscard.children[1].children[9].innerHTML.trim())
: statscard.children[1].children[9].children[0].innerHTML.trim()
},
realistic: {
victories: !statscard.children[2].children[1].children.length
? parseInt(statscard.children[2].children[1].innerHTML.trim())
: statscard.children[2].children[1].children[0].innerHTML.trim(),
completed: !statscard.children[2].children[2].children.length
? parseInt(statscard.children[2].children[2].innerHTML.trim())
: statscard.children[2].children[2].children[0].innerHTML.trim(),
ratio: !statscard.children[2].children[3].children.length
? statscard.children[2].children[3].innerHTML.trim()
: statscard.children[2].children[3].children[0].innerHTML.trim(),
deaths: !statscard.children[2].children[4].children.length
? parseInt(statscard.children[2].children[4].innerHTML.trim())
: statscard.children[2].children[4].children[0].innerHTML.trim(),
lions: !statscard.children[2].children[5].children.length
? statscard.children[2].children[5].innerHTML.trim()
: statscard.children[2].children[5].children[0].innerHTML.trim(),
playTime: !statscard.children[2].children[6].children.length
? statscard.children[2].children[6].innerHTML.trim()
: statscard.children[2].children[6].children[0].innerHTML.trim(),
airTargets: !statscard.children[2].children[7].children.length
? parseInt(statscard.children[2].children[7].innerHTML.trim())
: statscard.children[2].children[7].children[0].innerHTML.trim(),
groundTargets: !statscard.children[2].children[1].children.length
? parseInt(statscard.children[2].children[8].innerHTML.trim())
: statscard.children[2].children[8].children[0].innerHTML.trim(),
navalTargets: !statscard.children[2].children[9].children.length
? parseInt(statscard.children[2].children[9].innerHTML.trim())
: statscard.children[2].children[9].children[0].innerHTML.trim()
},
simulator: {
victories: !statscard.children[3].children[1].children.length
? parseInt(statscard.children[3].children[1].innerHTML.trim())
: statscard.children[3].children[1].children[0].innerHTML.trim(),
completed: !statscard.children[3].children[2].children.length
? parseInt(statscard.children[3].children[2].innerHTML.trim())
: statscard.children[3].children[2].children[0].innerHTML.trim(),
ratio: !statscard.children[3].children[3].children.length
? statscard.children[3].children[3].innerHTML.trim()
: statscard.children[3].children[3].children[0].innerHTML.trim(),
deaths: !statscard.children[3].children[4].children.length
? parseInt(statscard.children[3].children[4].innerHTML.trim())
: statscard.children[3].children[4].children[0].innerHTML.trim(),
lions: !statscard.children[3].children[5].children.length
? statscard.children[3].children[5].innerHTML.trim()
: statscard.children[3].children[5].children[0].innerHTML.trim(),
playTime: !statscard.children[3].children[6].children.length
? statscard.children[3].children[6].innerHTML.trim()
: statscard.children[3].children[6].children[0].innerHTML.trim(),
airTargets: !statscard.children[3].children[7].children.length
? parseInt(statscard.children[3].children[7].innerHTML.trim())
: statscard.children[3].children[7].children[0].innerHTML.trim(),
groundTargets: !statscard.children[3].children[1].children.length
? parseInt(statscard.children[3].children[8].innerHTML.trim())
: statscard.children[3].children[8].children[0].innerHTML.trim(),
navalTargets: !statscard.children[3].children[9].children.length
? parseInt(statscard.children[3].children[9].innerHTML.trim())
: statscard.children[3].children[9].children[0].innerHTML.trim()
}
};
/**
* Represents raw data about a player
* @typedef {Object} PlayerData
* @property {ProfileInfo} profile The player's profile
* @property {ProfileStats} stats Game statistics for the player
*/
const data = {
profile,
stats
};
return data;
}
parseClan() {
const { window: { document } } = new JSDOM(this.html);
const claninfo = document.getElementsByClassName("clan-info")[0];
const clanmembers = document.getElementsByClassName("clan-members")[0];
if (!claninfo) {
throw new TypeError("NOT_FOUND", "clan");
}
const members = [];
for (const member of clanmembers.children[0].children) {
if (member.children.length !== 7) {
continue;
}
/**
* Represents info about a clan member
* @typedef {Object} SquadronMemberInfo
* @property {string} name The in-game name of the squadron member
* @property {SquadronDifficultyStats} rating The rating for the squadron member, per difficulty
* @property {string} role The squadron member's role
* @property {string} entry The date of entry for the squadron member
*/
const obj = {
name: member.children[1].children[0].textContent,
rating: {
arcade: member.children[2].textContent,
realistic: member.children[3].textContent,
simulator: member.children[4].textContent
},
role: member.children[5].textContent !== "" ? member.children[5].textContent : "Unknown",
entry: member.children[6].textContent
};
members.push(obj);
}
/**
* Represents raw clan data
* @typedef {Object} SquadronData
* @property {string} name The squadron name
* @property {string} image The URL to the squadron's in-game picture
* @property {number} players The amount of players in the squadron
* @property {string} description The squadron's description
* @property {string} createdAt The date of creation for the squadron
* @property {SquadronDifficultyStats} airKills The amount of air targets destroyed per difficulty
* @property {SquadronDifficultyStats} groundKills The amount of ground targets destroyed per difficulty
* @property {SquadronDifficultyStats} deaths The amount of deaths per difficulty
* @property {SquadronDifficultyStats} flightTime The total flight time per difficulty
* @property {SquadronMemberInfo[]} members Info for each squadron member
*/
const data = {
name: claninfo.children[0].children[0].children[0].children[0].children[1].textContent.split(" ").slice(1).join(" "),
tag: claninfo.children[0].children[0].children[0].children[0].children[1].textContent.split("]")[0].slice(1),
image: claninfo.children[0].children[0].children[0].children[0].children[0].src.split(URLs.static).join(""),
players: parseInt(claninfo.children[0].children[0].children[0].children[0].children[2].textContent
.replace("Number of players:", "")
.split("<br>").join("")
.trim()),
description: claninfo.children[0].children[0].children[0].children[0].children[3].textContent,
createdAt: claninfo.children[0].children[0].children[0].children[0].children[4].textContent
.replace("date of creation:", "")
.trim(),
/**
* Represents info per difficulty for a clan.
* This is identical to {@link Profile#difficultyInfo}, with the difference being
* that {@link Profile#difficultyInfo} is for profile statistics (which are objects),
* and {@link Clan#SquadronDifficultyStats} is for strings.
* @typedef {Object} SquadronDifficultyStats
* @property {string} arcade The info for the Arcade gamemode
* @property {string} realistic The info for the Realistic gamemode
* @property {string} simulator The info for the Simulator gamemode
*/
airKills: {
arcade: claninfo.children[0].children[3].children[1].textContent,
realistic: claninfo.children[0].children[5].children[1].textContent,
simulator: claninfo.children[0].children[7].children[1].textContent
},
groundKills: {
arcade: claninfo.children[0].children[3].children[2].textContent,
realistic: claninfo.children[0].children[5].children[2].textContent,
simulator: claninfo.children[0].children[7].children[2].textContent
},
deaths: {
arcade: claninfo.children[0].children[3].children[3].textContent,
realistic: claninfo.children[0].children[5].children[3].textContent,
simulator: claninfo.children[0].children[7].children[3].textContent
},
flightTime: {
arcade: claninfo.children[0].children[3].children[4].textContent,
realistic: claninfo.children[0].children[5].children[4].textContent,
simulator: claninfo.children[0].children[7].children[4].textContent
},
members
};
return data;
}
parseNews() {
const { window: { document } } = new JSDOM(this.html);
const elements = document.getElementsByClassName("news_item");
if (!elements.length) throw new Error("NOT_FOUND", "news");
const data = [];
for (const element of elements) {
/**
* Represents a War Thunder news item
* @typedef {Object} NewsInfo
* @property {string} url The URL to the news/changelog page
* @property {string} title The title of the news/update
* @property {string} date The date the news was announced/the update was released
*/
const item = {
url: element.children[1].children[0].href,
title: element.children[1].children[0].textContent.split(" ").join(" "),
// eslint-disable-next-line max-len
date: element.children[0].textContent
};
data.push(item);
}
return data;
}
parseWiki() {
const { window: { document } } = new JSDOM(this.html);
if (document.getElementsByClassName("mw-search-nonefound").length) throw new Error("NOT_FOUND", "wiki");
const results = document.getElementsByClassName("mw-search-results")[0].children;
const data = [];
const main = document.getElementsByClassName("mw-search-createlink")[0];
if (main.textContent.trim() !== "") {
const arr = main.innerHTML
.split("<b>")
.join("")
.split("</b>")
.join("")
.split("\"\"")
.join("");
const title = arr.split(">")[1].split("<")[0].trim();
const obj = {
title,
url: `${URLs.wiki}${main.children[0].children[0].href}`,
main: true
};
data.push(obj);
}
for (const result of results) {
/**
* Represents a War Thunder Wiki search result
* @typedef {Object} wiki
* @property {string} title The title of the search result's page
* @property {string} url The URL to the search result's page
* @property {Boolean} main If there is a page named as the search query on the Wiki
*/
const obj = {
title: result.children[0].children[0].textContent,
url: `${URLs.wiki}${result.children[0].children[0].href}`,
main: false
};
if (data[0] && data[0].title === obj.title) continue;
data.push(obj);
}
return data;
}
}
module.exports = HTMLParser;