Coverage Summary for Class: WorldParser (com.galarzaa.tibiakt.core.section.community.world.parser)

Class Class, % Method, % Branch, % Line, % Instruction, %
WorldParser 100% (1/1) 100% (8/8) 66.7% (46/69) 93.2% (55/59) 88.3% (475/538)


 /*
  * 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.community.world.parser
 
 import com.galarzaa.tibiakt.core.collections.getContaining
 import com.galarzaa.tibiakt.core.collections.offsetStart
 import com.galarzaa.tibiakt.core.domain.world.BattlEyeType
 import com.galarzaa.tibiakt.core.domain.world.TransferType
 import com.galarzaa.tibiakt.core.enums.StringEnum
 import com.galarzaa.tibiakt.core.exceptions.ParsingException
 import com.galarzaa.tibiakt.core.html.parseTablesMap
 import com.galarzaa.tibiakt.core.html.rows
 import com.galarzaa.tibiakt.core.parser.Parser
 import com.galarzaa.tibiakt.core.section.community.world.builder.WorldBuilder
 import com.galarzaa.tibiakt.core.section.community.world.builder.worldBuilder
 import com.galarzaa.tibiakt.core.section.community.world.model.World
 import com.galarzaa.tibiakt.core.text.clean
 import com.galarzaa.tibiakt.core.text.parseInteger
 import com.galarzaa.tibiakt.core.text.remove
 import com.galarzaa.tibiakt.core.time.FORMAT_YEAR_MONTH
 import com.galarzaa.tibiakt.core.time.parseTibiaDateTime
 import com.galarzaa.tibiakt.core.time.parseTibiaFullDate
 import kotlinx.datetime.YearMonth
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Document
 import org.jsoup.nodes.Element
 
 /** Parser for world pages. */
 public object WorldParser : Parser<World?> {
     private val recordRegex = Regex("""(?<count>[\d.,]+) players \(on (?<date>[^)]+)\)""")
     private val battlEyeRegex = Regex("""since ([^.]+).""")
 
     override fun fromContent(content: String): World? {
         val document: Document = Jsoup.parse(content, "", org.jsoup.parser.Parser.xmlParser())
         val boxContent =
             document.selectFirst("div.BoxContent") ?: throw ParsingException("BoxContent container not found")
         val tables = boxContent.parseTablesMap("table.Table1, table.Table2")
         tables["Error"]?.apply { return null }
         val name = tables["World Selection"]?.selectFirst("option[selected]")?.text()?.clean()
             ?: throw ParsingException("World Selection table not found")
         val builder = worldBuilder {
             this.name = name
         }
         tables["World Information"]?.let { builder.parseWorldInformation(it) }
         tables.getContaining("Players Online")?.let { builder.parseOnlinePlayersTable(it) }
         return builder.build()
     }
 
     private fun WorldBuilder.parseOnlinePlayersTable(table: Element) {
         for (row in table.rows().offsetStart(2)) {
             val columns = row.select("td")
             val (name, level, vocation) = columns.map { it.text().clean() }
             addOnlinePlayer(name,
                 level.toInt(),
                 StringEnum.fromValue(vocation) ?: throw ParsingException("unknown vocation: $vocation"))
         }
     }
 
     private fun WorldBuilder.parseWorldInformation(table: Element) {
         for (row in table.rows().offsetStart(1)) {
             val columns = row.select("td")
             var (field, value) = columns.map { it.text().clean() }
             field = field.remove(":")
             when (field) {
                 "Status" -> isOnline = value.contains("online", true)
                 "Players Online" -> onlinePlayersCount = value.parseInteger()
                 "Online Record" -> parseOnlineRecord(value)
                 "Creation Date" -> parseCreationDate(value)
                 "Location" -> location = value
                 "PvP Type" -> pvpType =
                     StringEnum.fromValue(value) ?: throw ParsingException("unknown pvp type found: $value")
 
                 "Premium Type" -> isPremiumRestricted = true
                 "Transfer Type" -> parseTransferType(value)
                 "World Quest Titles" -> if (!value.contains("has no title", true)) {
                     value.split(",").map { worldQuest(it.clean()) }
                 }
 
                 "BattlEye Status" -> parseBattlEyeStatus(value)
                 "Game World Type" -> isExperimental = (value.contains("experimental", true))
             }
         }
     }
 
     private fun WorldBuilder.parseTransferType(value: String) {
         transferType = if (value.contains("locked", true)) {
             TransferType.LOCKED
         } else if (value.contains("blocked", true)) {
             TransferType.BLOCKED
         } else {
             TransferType.REGULAR
         }
 
     }
 
     private fun WorldBuilder.parseBattlEyeStatus(value: String) {
         battlEyeRegex.find(value)?.apply {
             val (_, since) = this.groupValues
             if (since.contains("release")) {
                 battlEyeType = BattlEyeType.GREEN
                 battlEyeStartedOn = null
             } else {
                 battlEyeType = BattlEyeType.YELLOW
                 battlEyeStartedOn = parseTibiaFullDate(since.clean())
             }
             return
         }
         battlEyeType = BattlEyeType.UNPROTECTED
         battlEyeStartedOn = null
     }
 
     private fun WorldBuilder.parseOnlineRecord(value: String) {
         recordRegex.find(value)?.apply {
             val (_, count, date) = this.groupValues
             onlineRecordCount = count.parseInteger()
             onlineRecordAt = parseTibiaDateTime(date)
 
         }
     }
 
     private fun WorldBuilder.parseCreationDate(value: String) {
         createdOn = YearMonth.parse(value, FORMAT_YEAR_MONTH)
 
     }
 }