util/HTMLParser.js

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;