Coverage Summary for Class: ForumBoardParser (com.galarzaa.tibiakt.core.section.forum.board.parser)

Class Class, % Method, % Branch, % Line, % Instruction, %
ForumBoardParser 100% (1/1) 100% (6/6) 64.9% (48/74) 98.4% (63/64) 92.1% (607/659)


 /*
  * 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.forum.board.parser
 
 import com.galarzaa.tibiakt.core.collections.offsetStart
 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.cleanText
 import com.galarzaa.tibiakt.core.html.formData
 import com.galarzaa.tibiakt.core.html.getLinkInformation
 import com.galarzaa.tibiakt.core.html.parseLastPostFromCell
 import com.galarzaa.tibiakt.core.html.parsePagination
 import com.galarzaa.tibiakt.core.html.rows
 import com.galarzaa.tibiakt.core.parser.Parser
 import com.galarzaa.tibiakt.core.section.forum.board.builder.ForumBoardBuilder
 import com.galarzaa.tibiakt.core.section.forum.board.builder.forumBoard
 import com.galarzaa.tibiakt.core.section.forum.board.model.ForumBoard
 import com.galarzaa.tibiakt.core.section.forum.shared.model.ForumEmoticon
 import com.galarzaa.tibiakt.core.section.forum.shared.model.ThreadStatus
 import com.galarzaa.tibiakt.core.text.remove
 import org.jsoup.nodes.Element
 
 /** Parser for forum boards. */
 public object ForumBoardParser : Parser<ForumBoard?> {
     private val fileNameRegex = Regex("""(\w+.gif)""")
     override fun fromContent(content: String): ForumBoard? {
         val boxContent = boxContent(content, org.jsoup.parser.Parser.xmlParser())
         val forumBreadcrumbs = boxContent.selectFirst("div.ForumBreadCrumbs")
         if (forumBreadcrumbs == null) {
             boxContent.selectFirst("div.InnerTableContainer")?.takeIf { "board you requested" in it.cleanText() }
                 ?: throw ParsingException("content does not belong to a board.")
             return null
         }
         return forumBoard {
             val headerText = forumBreadcrumbs.cleanText()
             val headerParts = headerText.split("|").map { it.trim() }
             name = headerParts.last()
             sectionName = headerParts.first()
             sectionId =
                 forumBreadcrumbs.selectFirst("a")?.getLinkInformation()?.queryParams?.get("sectionid")?.first()?.toInt()
                     ?: throw ParsingException("sectionId not found")
 
             val form = boxContent.selectFirst("form")?.formData() ?: throw ParsingException("form not found")
             boardId = form.values["boardid"]?.toInt()
             threadAge = form.values["threadage"]?.toInt() ?: return@forumBoard
 
             val table = boxContent.selectFirst("table.Table3") ?: throw ParsingException("No board tables found.")
             val contentTables = table.select(TABLE_SELECTOR)
             if (contentTables.size >= 2) parseAnnouncements(contentTables.first()!!)
             parseThreadsTable(contentTables.last()!!)
             val paginationData = boxContent.selectFirst("td > small")!!.parsePagination()
             currentPage = paginationData.currentPage
             totalPages = paginationData.totalPages
             resultsCount = paginationData.resultsCount
         }
     }
 
     private fun ForumBoardBuilder.parseThreadsTable(table: Element) {
         for (row in table.rows().offsetStart(1)) {
             val columns = row.cells()
             if (columns.size < 6) continue
             thread {
                 val (threadLink, pageLinks) = columns[2].select("a").mapNotNull { it.getLinkInformation() }
                     .let { it.first() to it.drop(1) }
                 var authorName = columns[3].cleanText()
                 columns[1].selectFirst("img")?.also {
                     emoticon = ForumEmoticon(it.attr("alt"), it.attr("src"))
                 }
                 val isTraded: Boolean
                 if ("(traded)" in authorName) {
                     authorName = authorName.replace("(traded)", "").trim()
                     isTraded = true
                 } else {
                     isTraded = false
                 }
                 val statusImageFileName = columns[0].selectFirst("img")?.attr("src")?.let {
                     fileNameRegex.find(it)?.groupValues?.get(1)
                 }.orEmpty()
                 val authorLink = columns[3].selectFirst("a")
 
                 title = threadLink.title
                 threadId = threadLink.queryParams["threadid"]?.get(0)?.toInt()
                 pages =
                     pageLinks.takeIf { it.isNotEmpty() }?.last()?.queryParams?.get("pagenumber")?.get(0)?.toInt() ?: 1
                 this.authorName = authorName
                 isAuthorTraded = isTraded
                 isAuthorDeleted = authorLink == null && !isTraded
                 ThreadStatus.values().filter { it.name.lowercase() in statusImageFileName }.forEach { status.add(it) }
                 lastPost = parseLastPostFromCell(columns[6]) ?: throw ParsingException("last post not found")
                 repliesCount = columns[4].text().remove(",").toInt()
                 viewsCount = columns[5].text().remove(",").toInt()
                 hasGoldenFrame = "ClassifiedProposal" in row.attr("class")
             }
         }
     }
 
     private fun ForumBoardBuilder.parseAnnouncements(table: Element) {
         for (row in table.rows().offsetStart(1)) {
             val (authorLink, titleLink) = row.select("a").mapNotNull { it.getLinkInformation() }
             announcement {
                 title = titleLink.title
                 author = authorLink.title
                 announcementId = titleLink.queryParams["announcementid"]?.first()?.toInt()
             }
         }
     }
 }