Coverage Summary for Class: AuctionParser (com.galarzaa.tibiakt.core.section.charactertrade.bazaar.parser)

Class Class, % Method, % Branch, % Line, % Instruction, %
AuctionParser 100% (1/1) 92.6% (25/27) 61.8% (157/254) 81.6% (261/320) 78.4% (2302/2936)


 /*
  * Copyright © 2025 Allan Galarza
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 package com.galarzaa.tibiakt.core.section.charactertrade.bazaar.parser
 
 import com.galarzaa.tibiakt.core.collections.offsetStart
 import com.galarzaa.tibiakt.core.enums.StringEnum
 import com.galarzaa.tibiakt.core.exceptions.ParsingException
 import com.galarzaa.tibiakt.core.html.TABLE_SELECTOR
 import com.galarzaa.tibiakt.core.html.cells
 import com.galarzaa.tibiakt.core.html.cellsText
 import com.galarzaa.tibiakt.core.html.cleanText
 import com.galarzaa.tibiakt.core.html.getLinkInformation
 import com.galarzaa.tibiakt.core.html.parsePagination
 import com.galarzaa.tibiakt.core.html.parseTablesMap
 import com.galarzaa.tibiakt.core.html.replaceBrs
 import com.galarzaa.tibiakt.core.html.rows
 import com.galarzaa.tibiakt.core.html.wholeCleanText
 import com.galarzaa.tibiakt.core.parser.Parser
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.builder.AuctionBuilder
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.builder.auction
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.AchievementEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.Auction
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.AuctionSkills
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.AuctionStatus
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.BestiaryEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.BlessingEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.BosstiaryEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.CharmEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.FamiliarEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.Familiars
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.FragmentProgressEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.ItemEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.ItemSummary
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.MountEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.Mounts
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.OutfitEntry
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.OutfitImage
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.Outfits
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.RevealedGem
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.RevealedGemMod
 import com.galarzaa.tibiakt.core.section.charactertrade.bazaar.model.WeaponProficiency
 import com.galarzaa.tibiakt.core.text.clean
 import com.galarzaa.tibiakt.core.text.parseInteger
 import com.galarzaa.tibiakt.core.text.parseLong
 import com.galarzaa.tibiakt.core.text.parseRomanNumerals
 import com.galarzaa.tibiakt.core.text.remove
 import com.galarzaa.tibiakt.core.time.parseTibiaDateTime
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Element
 import java.io.File
 import java.net.URL
 
 /** Parser for auction pages. */
 public object AuctionParser : Parser<Auction?> {
     private const val PERCENTAGE = 100f
 
     @PublishedApi
     internal const val ICON_CSS_SELECTOR: String = "div.CVIcon"
     internal const val PAGINATOR_SELECTOR: String = "div.BlockPageNavigationRow"
 
     private val charInfoRegex = Regex("""Level: (\d+) \| Vocation: ([\w\s]+)\| (\w+) \| World: (\w+)""")
     private val idAddonsRegex = Regex("""/(\d+)_(\d+)""")
     private val amountRegex = Regex("""([\d,]+)x """)
     private val tierRegex = Regex("""(.*)\s\(tier (\d)\)""")
     private val idRegex = Regex("""(\d+).(?:gif|png)""")
 
     override fun fromContent(content: String): Auction? = fromContent(content, 0, true)
 
     /**
      * Parse the content of an auction.
      *
      * @param content The HTML content of the auction's page.
      * @param auctionId The auction ID to set to the results, since it is not possible to know from the content only.
      * @param parseDetails Whether to parse the auction's detail or just the general information.
      */
     public fun fromContent(content: String, auctionId: Int, parseDetails: Boolean = true): Auction? {
         val document = Jsoup.parse(content)
         return auction {
             this.auctionId = auctionId
             document.selectFirst("div.Auction")?.let { parseAuctionContainer(it) } ?: return null
             val detailsTables = document.parseTablesMap("table.Table3, table.Table5")
             if (!parseDetails) return@auction
 
             details {
                 detailsTables["General"]?.apply { parseGeneralTable(this) }
                 detailsTables["Item Summary"]?.apply { items = parseItemsTable(this) }
                 detailsTables["Store Item Summary"]?.apply { storeItems = parseItemsTable(this) }
                 detailsTables["Mounts"]?.apply { mounts = parseMountsTable(this) }
                 detailsTables["Store Mounts"]?.apply { storeMounts = parseMountsTable(this) }
                 detailsTables["Outfits"]?.apply { outfits = parseOutfitsTable(this) }
                 detailsTables["Store Outfits"]?.apply { storeOutfits = parseOutfitsTable(this) }
                 detailsTables["Familiars"]?.apply { familiars = parseFamiliarsTable(this) }
                 detailsTables["Blessings"]?.apply { parseBlessingsTable(this) }
                 detailsTables["Imbuements"]?.apply { parseSingleColumnTable(this).forEach { addImbuement(it) } }
                 detailsTables["Charms"]?.apply { parseCharmsTable(this) }
                 detailsTables["Completed Cyclopedia Map Areas"]?.apply {
                     parseSingleColumnTable(this).forEach { addCompletedCyclopediaMapArea(it) }
                 }
                 detailsTables["Completed Quest Lines"]?.apply {
                     parseSingleColumnTable(this).forEach { addCompletedQuestLine(it) }
                 }
                 detailsTables["Titles"]?.run { parseSingleColumnTable(this).forEach { addTitle(it) } }
                 detailsTables["Achievements"]?.run { parseAchievementsTable(this) }
                 detailsTables["Bestiary Progress"]?.run { parseBestiaryTable(this) }
                 detailsTables["Bosstiary Progress"]?.run { parseBosstiaryTablee(this) }
                 detailsTables["Revealed Gems"]?.run { parseRevealedGemsTable(this) }
                 detailsTables["Fragment Progress"]?.run { parseFragmentProgressTable(this) }
                 detailsTables["Proficiencies"]?.run { parseProficienciesTable(this) }
             }
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseCharmsTable(table: Element) {
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val colsText = row.cellsText()
             if (colsText.size != 4) continue
             val (cost, type, name, grade) = colsText
             val icon = row.selectFirst("img") ?: continue
             addCharm(CharmEntry(name, cost.parseInteger(), icon.attr("alt"), type, grade.parseInteger()))
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseAchievementsTable(table: Element) {
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val text = row.cleanText()
             if (text.contains("more entries")) continue
             val isSecret = row.selectFirst("img") != null
             addAchievement(AchievementEntry(text, isSecret))
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseBosstiaryTablee(table: Element) {
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val columnsText = row.cellsText()
             if (columnsText.size != 3) continue
             val (step, kills, name) = columnsText
             val entry = BosstiaryEntry(name, kills.remove("x").parseLong(), step.toInt())
             addBosstiaryEntry(entry)
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseBestiaryTable(table: Element) {
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val columnsText = row.cellsText()
             if (columnsText.size != 4) continue
             val (step, kills, name, _) = columnsText
             val masteryIcon = row.selectFirst("img") ?: continue
             val masteryUnlocked = masteryIcon.attr("alt").contains("unlocked")
             val entry = BestiaryEntry(name, kills.remove("x").parseLong(), step.toInt(), masteryUnlocked)
             addBestiaryEntry(entry)
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseRevealedGemsTable(table: Element) {
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val gemTag = row.selectFirst("div.Gem") ?: continue
             val gemType = gemTag.attr("title")
             val modSpans = row.select("span")
             val mods = modSpans.map {
                 RevealedGemMod(it.replaceBrs().wholeCleanText().split("\n"))
             }
             addRevealedGem(RevealedGem(gemType, mods))
         }
     }
     private fun AuctionBuilder.AuctionDetailsBuilder.parseFragmentProgressTable(table: Element) {
         var modType: String? = null
         for (row in table.select(TABLE_SELECTOR).flatMap { it.rows() }) {
             val columns = row.cells()
             if (columns.size != 2) continue
             val (gradeCol, typeCol) = columns
             if(gradeCol.text().contains("Grade")) {
                 modType = typeCol.text().remove(" Mod")
                 continue
             }
             if(modType == null) continue
             val grade = parseRomanNumerals(gradeCol.text())
             addFragmentProgress(FragmentProgressEntry(grade, typeCol.cleanText(), modType))
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseProficienciesTable(table: Element){
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val columns = row.cells()
             if (columns.size != 4) continue
             val (weaponCell, levelCell, progressCell, masteryCell) = columns
             val (level, maxLevel) = levelCell.cleanText().split("/").map { it.toInt() }
             val masteryIcon = masteryCell.selectFirst("img") ?: continue
             val masteryUnlocked = masteryIcon.attr("alt").contains("unlocked")
             addProficiency(
                 WeaponProficiency(
                     name = weaponCell.cleanText(),
                     level = level,
                     maxLevel = maxLevel,
                     totalProgress = progressCell.cleanText().parseInteger(),
                     hasMastery = masteryUnlocked
                 )
             )
         }
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseBlessingsTable(table: Element) {
         for (row in table.selectFirst(TABLE_SELECTOR).rows().offsetStart(1)) {
             val colsText = row.cellsText()
             if (colsText.size != 2) continue
             val (amount, name) = colsText
             addBlessing(BlessingEntry(name, amount.remove("x").parseInteger()))
         }
     }
 
     private fun parseSingleColumnTable(table: Element): List<String> {
         val innerTable = table.select(TABLE_SELECTOR).last()
         val values = mutableListOf<String>()
         for (row in innerTable.rows().offsetStart(1)) {
             val text = row.selectFirst("td")?.cleanText().orEmpty()
             if (text.contains("more entries")) continue
             values.add(text)
         }
         return values
     }
 
     private fun parseFamiliarsTable(table: Element): Familiars {
         val paginationData = table.selectFirst(PAGINATOR_SELECTOR)?.parsePagination() ?: return Familiars(
             1,
             0,
             0,
             emptyList(),
             false
         )
         val familiarBoxes = table.select(ICON_CSS_SELECTOR)
         val familiars = mutableListOf<FamiliarEntry>()
         for (mountBox in familiarBoxes) {
             val name = mountBox.attr("title").split("(").first().clean()
             val (_, id) = idRegex.find(mountBox.selectFirst("img")?.attr("src").orEmpty())?.groupValues
                 ?: throw ParsingException("familiar image did not match expected pattern")
             familiars.add(FamiliarEntry(name, id.toInt()))
         }
         return Familiars(
             paginationData.currentPage,
             paginationData.totalPages,
             paginationData.resultsCount,
             familiars,
             false
         )
     }
 
     private fun parseOutfitsTable(table: Element): Outfits {
         val paginationData = table.selectFirst(PAGINATOR_SELECTOR)?.parsePagination() ?: return Outfits(
             1,
             0,
             0,
             emptyList(),
             false
         )
         val outfitBoxes = table.select(ICON_CSS_SELECTOR)
         val outfits = outfitBoxes.map { parseDisplayedOutfit(it) }
         return Outfits(
             paginationData.currentPage,
             paginationData.totalPages,
             paginationData.resultsCount,
             outfits,
             false
         )
     }
 
     private fun parseMountsTable(mountsTable: Element): Mounts {
         val paginationData =
             mountsTable.selectFirst(PAGINATOR_SELECTOR)?.parsePagination() ?: return Mounts(
                 1,
                 0,
                 0,
                 emptyList(),
                 false
             )
         val mountsBoxes = mountsTable.select(ICON_CSS_SELECTOR)
         val mounts = mountsBoxes.map { parseDisplayedMount(it) }
         return Mounts(paginationData.currentPage, paginationData.totalPages, paginationData.resultsCount, mounts, false)
     }
 
     private fun parseItemsTable(itemsTable: Element): ItemSummary {
         val paginationData =
             itemsTable.selectFirst(PAGINATOR_SELECTOR)?.parsePagination() ?: return ItemSummary(
                 1,
                 0,
                 0,
                 emptyList(),
                 false
             )
         val itemBoxes = itemsTable.select(ICON_CSS_SELECTOR)
         val items = mutableListOf<ItemEntry>()
         for (itemBox in itemBoxes) {
             parseDisplayedItem(itemBox)?.run { items.add(this) }
         }
         return ItemSummary(
             paginationData.currentPage,
             paginationData.totalPages,
             paginationData.resultsCount,
             items,
             false
         )
     }
 
     private fun getAuctionTableFieldValue(row: Element): Pair<String, String> {
         val field =
             row.selectFirst(".LabelV, .LabelColumn")?.cleanText()?.remove(":")
                 ?: throw ParsingException("could not find field's label")
         val value = row.selectFirst("div")?.cleanText() ?: throw ParsingException("could not find value's div")
         return field to value
     }
 
     private fun parseSkillValue(row: Element): Double {
         val (_, level, progress) = row.cellsText()
         return level.parseInteger() + (progress.remove("%").toDouble() / PERCENTAGE)
     }
 
     private fun AuctionBuilder.AuctionDetailsBuilder.parseGeneralTable(table: Element) {
         val skillsMap = mutableMapOf<String, Double>().withDefault { 0.0 }
         val contentContainers = table.select(TABLE_SELECTOR)
         contentContainers.rows().forEach { row ->
             val (field, value) = getAuctionTableFieldValue(row)
             when (field) {
                 "Hit Points" -> hitPoints = value.parseInteger()
                 "Mana" -> mana = value.parseInteger()
                 "Capacity" -> capacity = value.parseInteger()
                 "Speed" -> speed = value.parseInteger()
                 "Blessings" -> blessingsCount = value.split("/").first().parseInteger()
                 "Mounts" -> mountsCount = value.parseInteger()
                 "Outfits" -> outfitsCount = value.parseInteger()
                 "Titles" -> titlesCount = value.parseInteger()
 
                 "Axe Fighting" -> skillsMap[field] = parseSkillValue(row)
                 "Club Fighting" -> skillsMap[field] = parseSkillValue(row)
                 "Sword Fighting" -> skillsMap[field] = parseSkillValue(row)
                 "Distance Fighting" -> skillsMap[field] = parseSkillValue(row)
                 "Fishing" -> skillsMap[field] = parseSkillValue(row)
                 "Magic Level" -> skillsMap[field] = parseSkillValue(row)
                 "Fist Fighting" -> skillsMap[field] = parseSkillValue(row)
                 "Shielding" -> skillsMap[field] = parseSkillValue(row)
 
                 "Creation Date" -> characterCreatedAt = parseTibiaDateTime(value)
                 "Experience" -> experience = value.parseLong()
                 "Gold" -> gold = value.parseLong()
                 "Achievement Points" -> achievementPoints = value.parseInteger()
 
                 "Regular World Transfer" -> if (value.contains("after")) {
                     regularWorldTransfersUnlockAt = parseTibiaDateTime(value.split("after")[1])
                 }
 
                 "Charm Expansion" -> hasCharmExpansion = value.contains("yes")
                 "Available Charm Points" -> availableCharmPoints = value.parseInteger()
                 "Spent Charm Points" -> spentCharmPoints = value.parseInteger()
                 "Available Minor Charm Echoes" -> availableMinorCharmEchoes = value.parseInteger()
                 "Spent Minor Charm Echoes" -> spentMinorCharmEchoes = value.parseInteger()
 
                 "Daily Reward Streak" -> dailyRewardStreak = value.parseInteger()
 
                 "Hunting Task Points" -> huntingTaskPoints = value.parseInteger()
                 "Permanent Weekly Task Expansion" -> permanentWeeklyTaskExpansion = value.contains("yes")
                 "Permanent Prey Slots" -> permanentPreySlots = value.parseInteger()
                 "Prey Wildcards" -> preyWildcards = value.parseInteger()
 
                 "Hirelings" -> hirelings = value.parseInteger()
                 "Hireling Jobs" -> hirelingJobs = value.parseInteger()
                 "Hireling Outfits" -> hirelingOutfits = value.parseInteger()
 
                 "Exalted Dust" -> run {
                     val (current, limit) = value.split("/").map { it.parseInteger() }
                     exaltedDust = current
                     exaltedDustLimit = limit
                 }
 
                 "Animus Masteries unlocked" -> animusMasteriesUnlocked = value.parseInteger()
 
                 "Boss Points" -> bossPoints = value.parseInteger()
 
                 "Bonus Promotion Points" -> bonusPromotionPoints = value.parseInteger()
             }
         }
 
         skills = AuctionSkills(
             axeFighting = skillsMap.getValue("Axe Fighting"),
             clubFighting = skillsMap.getValue("Club Fighting"),
             swordFighting = skillsMap.getValue("Sword Fighting"),
             distanceFighting = skillsMap.getValue("Distance Fighting"),
             fishing = skillsMap.getValue("Fishing"),
             magicLevel = skillsMap.getValue("Magic Level"),
             fistFighting = skillsMap.getValue("Fist Fighting"),
             shielding = skillsMap.getValue("Shielding"),
         )
     }
 
     internal fun AuctionBuilder.parseAuctionContainer(auctionContainer: Element) {
         val headerContainer =
             auctionContainer.selectFirst("div.AuctionHeader") ?: throw ParsingException("auction header not found")
         name = headerContainer.selectFirst("div.AuctionCharacterName")?.cleanText()
             ?: throw ParsingException("character name not found")
         headerContainer.selectFirst("div.AuctionCharacterName > a")?.apply {
             val auctionLinkInfo = getLinkInformation()!!
             auctionId = auctionLinkInfo.queryParams["auctionid"]?.first()?.toInt()
                 ?: throw ParsingException("auctionId not found in link")
             remove()
         }
 
         charInfoRegex.find(headerContainer.cleanText())?.run {
             val (_, levelStr, vocationStr, sexStr, worldStr) = groupValues
             level = levelStr.toInt()
             vocation =
                 StringEnum.Companion.fromValue(vocationStr.trim())
                     ?: throw ParsingException("Unknown vocation found: $vocationStr")
             sex =
                 StringEnum.Companion.fromValue(sexStr.lowercase().trim())
                     ?: throw ParsingException("Unknown sex found: $sexStr")
             world = worldStr.trim()
         }
         val outfitImageUrl = auctionContainer.selectFirst("img.AuctionOutfitImage")?.attr("src")
             ?: throw ParsingException("outfit image not found")
         val (_, outfitId, addons) = idAddonsRegex.find(outfitImageUrl)?.groupValues
             ?: throw ParsingException("image URL does not match expected pattern")
 
         auctionContainer.select(".CVIcon").forEach {
             parseDisplayedItem(it)?.run { addDisplayedItem(this) }
         }
 
         outfit = OutfitImage(outfitId = outfitId.toInt(), addons = addons.toInt())
 
         auctionContainer.select("div.Entry").forEach {
             val img = it.select("img")
             val imgUrl = img.attr("src")
             val (_, id) = idRegex.find(imgUrl)?.groupValues
                 ?: throw ParsingException("sales argument image does not match pattern.")
             salesArgument {
                 categoryId = id.toInt()
                 content = it.cleanText()
             }
         }
 
         val (startDate, endDate, _) = auctionContainer.select("div.ShortAuctionDataValue").map { it.cleanText() }
         startsAt = parseTibiaDateTime(startDate)
         endsAt = parseTibiaDateTime(endDate)
         auctionContainer.selectFirst("div.ShortAuctionDataBidRow")?.run {
             val bidTag = selectFirst("div.ShortAuctionDataValue") ?: throw ParsingException("Could not find bid")
             val bidTypeTag =
                 selectFirst("div.ShortAuctionDataLabel") ?: throw ParsingException("could not find bid type")
             val bidTypeStr = bidTypeTag.cleanText().remove(":")
             bid = bidTag.text().parseInteger()
             bidType =
                 StringEnum.Companion.fromValue(bidTypeStr) ?: throw ParsingException("unknown bid type: $bidTypeStr")
         }
 
         status =
             auctionContainer.selectFirst("div.AuctionInfo")?.cleanText()?.let { StringEnum.Companion.fromValue(it) }
             ?: AuctionStatus.IN_PROGRESS
     }
 
     @PublishedApi
     internal fun parseDisplayedItem(displayItemContainer: Element): ItemEntry? {
         val title = displayItemContainer.attr("title")
         val fileUrl = displayItemContainer.selectFirst("img")?.attr("src") ?: return null
         val fileName = File(URL(fileUrl).path).name
         val itemId = fileName.remove(".gif").toInt()
         val titleLines = title.split("\n")
         var name = titleLines.first()
         val description = if (titleLines.size > 1) {
             titleLines.offsetStart(1).joinToString("\n")
         } else {
             null
         }
         var amount = 1
         amountRegex.find(name)?.apply {
             amount = groups[1]!!.value.parseInteger()
             name = amountRegex.replace(name, "")
         }
         var tier = 0
         tierRegex.find(name)?.apply {
             tier = groups[2]!!.value.parseInteger()
             name = groups[1]!!.value
         }
         return ItemEntry(itemId, name, description, amount, tier)
     }
 
     @PublishedApi
     internal fun parseDisplayedOutfit(container: Element): OutfitEntry {
         val name = container.attr("title").split("(").first().clean()
         val (_, id, addons) = idAddonsRegex.find(container.selectFirst("img")?.attr("src").orEmpty())?.groupValues
             ?: throw ParsingException("outfit image did not match expected pattern")
         return OutfitEntry(name, id.toInt(), addons.toInt())
     }
 
     @PublishedApi
     internal fun parseDisplayedMount(container: Element): MountEntry {
         val name = container.attr("title")
         val (_, id) = idRegex.find(container.selectFirst("img")?.attr("src").orEmpty())?.groupValues
             ?: throw ParsingException("mount image did not match expected pattern")
         return MountEntry(name, id.toInt())
     }
 
     /**
      * Parses the items from the dynamic paginator in an auction's details page.
      */
     public inline fun <reified T> parsePageItems(content: String): List<T> {
         val document = Jsoup.parse(content)
         val cvIcon = document.select(ICON_CSS_SELECTOR)
         return when (T::class) {
             ItemEntry::class -> cvIcon.mapNotNull { parseDisplayedItem(it) as T }
             OutfitEntry::class -> cvIcon.mapNotNull { parseDisplayedOutfit(it) as T }
             MountEntry::class -> cvIcon.mapNotNull { parseDisplayedMount(it) as T }
             else -> emptyList()
         }
     }
 }