Coverage Summary for Class: ForumThreadParser (com.galarzaa.tibiakt.core.section.forum.thread.parser)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ForumThreadParser |
100%
(1/1)
|
100%
(5/5)
|
57.1%
(36/63)
|
86.9%
(73/84)
|
82.6%
(587/711)
|
/*
* 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.thread.parser
import com.galarzaa.tibiakt.core.exceptions.ParsingException
import com.galarzaa.tibiakt.core.html.TABLE_SELECTOR
import com.galarzaa.tibiakt.core.html.cleanText
import com.galarzaa.tibiakt.core.html.getLinkInformation
import com.galarzaa.tibiakt.core.html.parseAuthorTable
import com.galarzaa.tibiakt.core.html.parsePagination
import com.galarzaa.tibiakt.core.parser.Parser
import com.galarzaa.tibiakt.core.section.forum.shared.model.ForumEmoticon
import com.galarzaa.tibiakt.core.section.forum.thread.builder.ForumThreadBuilder
import com.galarzaa.tibiakt.core.section.forum.thread.builder.forumThread
import com.galarzaa.tibiakt.core.section.forum.thread.model.ForumThread
import com.galarzaa.tibiakt.core.time.parseTibiaForumDateTime
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
/** Parser for forum threads. */
public object ForumThreadParser : Parser<ForumThread?> {
private const val SIGNATURE_SEPARATOR = "________________"
private val datesRegex = Regex("""(\d{2}\.\d{2}\.\d{4}\s\d{2}:\d{2}:\d{2})""")
private val editedByRegex = Regex("""Edited by (.*) on \d{2}""")
override fun fromContent(content: String): ForumThread? {
val boxContent = boxContent(content)
return forumThread {
val breadCrumbs = boxContent.select("div.ForumBreadCrumbs > a")
if (breadCrumbs.isEmpty()) {
val messageBox = boxContent.selectFirst("div.InnerTableContainer")
if (messageBox == null || "not found" !in messageBox.text()) {
throw ParsingException("Could not find Forum Breadcrumbs")
}
return null
}
val (sectionLink, boardLink) = breadCrumbs.mapNotNull { it.getLinkInformation() }
sectionId = sectionLink.queryParams["sectionid"]?.first()?.toInt()
?: throw ParsingException("Could not find section ID in link.")
sectionName = sectionLink.title
boardId = boardLink.queryParams["boardid"]?.first()?.toInt()
?: throw ParsingException("Could not find board ID in link.")
boardName = boardLink.title
val forumTitleContainer = boxContent.selectFirst("div.ForumTitleText")
if (forumTitleContainer == null) {
title = boxContent.selectFirst("div.ForumBreadCrumbs > b")?.text()
?: throw ParsingException("Could not find partial title")
threadId = 0
currentPage = -1
return@forumThread
}
val border = forumTitleContainer.parent()!!.previousSibling()!!.previousSibling()!!
hasGoldenFrame = "gold" in border.attr("style")
title = forumTitleContainer.cleanText()
val postTable = boxContent.selectFirst(TABLE_SELECTOR)!!
val threadInfoContainer = postTable.selectFirst("div.ForumPostHeaderText")!!
val (threadNumber, navigationContainer) = threadInfoContainer.childNodes()
threadId = (threadNumber as TextNode).cleanText().split("#").last().toInt()
val navigationLinks = (navigationContainer as Element).select("a").mapNotNull { it.getLinkInformation() }
if (navigationLinks.size == 2) {
val (prevLink, nextLink) = navigationLinks
previousTopicNumber = prevLink.queryParams["threadid"]?.first()?.toInt()
?: throw ParsingException("Could not find previous topic number in link.")
nextTopicNumber = nextLink.queryParams["threadid"]?.first()?.toInt()
?: throw ParsingException("Could not find next topic number in link.")
} else if ("Previous" in navigationLinks.first().title) {
previousTopicNumber = navigationLinks.first().queryParams["threadid"]?.first()?.toInt()
?: throw ParsingException("Could not find previous topic number in link.")
} else {
nextTopicNumber = navigationLinks.first().queryParams["threadid"]?.first()?.toInt()
?: throw ParsingException("Could not find next topic number in link.")
}
val paginationData = boxContent.selectFirst("td.PageNavigation")!!.parsePagination()
totalPages = paginationData.totalPages
currentPage = paginationData.currentPage
resultsCount = paginationData.resultsCount
val postContainers = postTable.select("div.PostBody")
for (postContainer in postContainers) {
parsePostContainer(postContainer)
}
}
}
private fun ForumThreadBuilder.parsePostContainer(container: Element) {
addPost {
author = parseAuthorTable(container.selectFirst("div.PostCharacterText")!!)
val contentContainer = container.selectFirst("div.PostText")!!
var emoticonTag: Element? = null
var titleTag: Element? = null
while (true) {
val child = contentContainer.child(0)
child.remove()
when (child.tagName()) {
"img" -> emoticonTag = child
"b" -> titleTag = child
"div" -> break
}
}
contentContainer.selectFirst("br")?.remove()
val signatureContainer = container.selectFirst("td.ff_pagetext")
if (signatureContainer != null) {
signature = signatureContainer.html()
signatureContainer.remove()
}
content = contentContainer.html()
if (signatureContainer != null) {
val parts = content!!.split(SIGNATURE_SEPARATOR)
content = parts.subList(0, parts.size - 1).joinToString(SIGNATURE_SEPARATOR)
}
emoticonTag?.let {
emoticon = ForumEmoticon(it.attr("alt"), it.attr("src"))
}
titleTag?.let {
title = it.cleanText()
}
val postDetails = container.selectFirst("div.PostDetails")!!
val dates = datesRegex.findAll(postDetails.text()).map { it.value }.toList()
postedDate = parseTibiaForumDateTime(dates.first())
if (dates.size > 1) {
editedDate = parseTibiaForumDateTime(dates[1])
editedBy = editedByRegex.find(postDetails.text())!!.groupValues[1]
}
val additionalBox = container.selectFirst("div.AdditionalBox")!!
postId = additionalBox.cleanText().split("#", limit = 2)[1].toInt()
}
}
}