Coverage Summary for Class: ElementsKt (com.galarzaa.tibiakt.core.html)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ElementsKt |
100%
(1/1)
|
89.5%
(17/19)
|
63.2%
(43/68)
|
93.8%
(90/96)
|
88.6%
(774/874)
|
/*
* 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.html
import com.galarzaa.tibiakt.core.exceptions.ParsingException
import com.galarzaa.tibiakt.core.text.clean
import com.galarzaa.tibiakt.core.text.parseInteger
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import org.jsoup.parser.Parser
import org.jsoup.select.Elements
import java.net.URL
internal const val TABLE_SELECTOR = "table.TableContent"
internal fun Element.boxContent(): Element =
selectFirst("div.BoxContent") ?: throw ParsingException("BoxContent container not found")
internal fun Element.parseTables(contentTableSelector: String = TABLE_SELECTOR): Map<String, Elements> {
val tables = select("div.TableContainer")
val output = mutableMapOf<String, Elements>()
for (table: Element in tables) {
val captionContainer = table.selectFirst("div.CaptionContainer")
val contentTable = table.selectFirst(contentTableSelector)
val caption = captionContainer?.text() ?: throw ParsingException("table has no caption container")
if (contentTable == null) continue
val rows = contentTable.select("tr")
output[caption] = rows
}
return output
}
internal fun Element.parseTablesMap(contentSelector: String = "div.TableContentContainer"): Map<String, Element> {
val tables = select("div.TableContainer")
val output = mutableMapOf<String, Element>()
for (table: Element in tables) {
val caption: String =
table.selectFirst("div.Text")?.cleanText() ?: throw ParsingException("table has no caption")
val contentTable = table.selectFirst(contentSelector)
output[caption] = contentTable ?: continue
}
return output
}
/** Get a list of rows in the element. */
internal fun Element?.rows(): Elements = this?.select("tr") ?: Elements()
internal fun Elements?.rows(): Elements = this?.select("tr") ?: Elements()
/** Get a list of cells found in the elmenet. */
internal fun Element?.cells(): Elements = this?.select("td") ?: Elements()
/** Get a list of the text contained in all cells in the element.*/
internal fun Element.cellsText(): List<String> = cells().map { it.cleanText() }
/** Get the text contained in an element, cleaned out of extraneous characters. */
internal fun Element.cleanText() = text().clean()
internal fun TextNode.cleanText() = text().clean()
/** Get the text contained in a list of element, cleaned out of extraneous characters. */
internal fun Elements.cleanText() = text().clean()
/** Get the text contained in an element, cleaned out of extraneous characters. */
internal fun Element.wholeCleanText() = wholeText().clean()
/** Replace all br tags in an element with line jumps. */
internal fun Element.replaceBrs() = apply {
select("br").forEach { it.replaceWith(TextNode("\n")) }
}
/** Replace all br tags in an array of elements with line jumps. */
internal fun ArrayList<Element>.replaceBr() = forEach { it.replaceBrs() }
/** Get all the field's values and available options of a form element.
*
* @receiver An element with the form tag.
*/
internal fun Element.formData(): FormData {
require(this.tagName().lowercase() == "form") {
"expected element with 'form' tag, got element with '${this.tagName()}' tag"
}
val data = mutableMapOf<String, String>()
val dataMultiple = mutableMapOf<String, MutableList<String>>()
val availableOptions = mutableMapOf<String, MutableList<String>>()
select("input[type=text], input[type=hidden]").forEach { data[it.attr("name")] = it.attr("value") }
select("select").forEach {
it.select("option").forEach { opt ->
val value = opt.attr("value")
val name = it.attr("name")
availableOptions.getOrPut(name) { mutableListOf() }.add(value)
if (opt.hasAttr("selected")) data[name] = value
}
}
select("input[type=checkbox]").forEach {
val name = it.attr("name")
val value = it.attr("value")
availableOptions.getOrPut(name) { mutableListOf() }.add(value)
if (it.hasAttr("checked")) dataMultiple.getOrPut(name) { mutableListOf() }.add(value)
}
select("input[type=radio]").forEach {
val name = it.attr("name")
val value = it.attr("value")
availableOptions.getOrPut(name) { mutableListOf() }.add(value)
if (it.hasAttr("checked")) data[name] = value
}
return FormData(data, dataMultiple, availableOptions, action = attr("action"), method = attr("method"))
}
private val pageRegex = Regex("""(?:page|pagenumber)=(\d+)""")
private val resultsRegex = Regex("""Results: ([\d,]+)""")
/** Parse the pagination block present in many Tibia.com sections. */
internal fun Element.parsePagination(): PaginationData {
val (pagesDiv, resultsDiv) = select("small > div")
val currentPageLink = pagesDiv.selectFirst(".CurrentPageLink")
val pageLinks = pagesDiv.select(".PageLink")
val firstOrLastPages = pagesDiv.select(".FirstOrLastElement")
val totalPages = if (firstOrLastPages.isNotEmpty()) {
val lastPageLink = firstOrLastPages.last()?.selectFirst("a")
if (lastPageLink != null) {
pageRegex.find(lastPageLink.attr("href"))?.let {
it.groups[1]!!.value.toInt()
} ?: throw ParsingException("Could not parse pagination info")
} else {
pageLinks[pageLinks.size - 2].text().toInt()
}
} else {
pageLinks.last()?.text()?.toInt() ?: throw ParsingException("could not find last page link")
}
val page = try {
currentPageLink?.text()?.toInt() ?: totalPages
} catch (nfe: NumberFormatException) {
if (currentPageLink?.text()?.contains("First") == true) 1
else totalPages
}
val resultsCount: Int = resultsRegex.find(resultsDiv.text())?.let {
it.groups[1]!!.value.parseInteger()
} ?: 0
return PaginationData(page, totalPages, resultsCount)
}
internal fun parsePopup(content: String): Pair<String, Document> {
val parts = content.split(",", limit = 3)
val title = parts[1].replace("'", "").trim()
val html = parts[parts.size - 1].replace("\\'", "\"").replace("'", "").replace(",);", "").replace(", );", "").trim()
val parsedHtml = Jsoup.parse(html, "", Parser.xmlParser())
return title to parsedHtml
}
private val queryStringRegex = Regex("([^&=]+)=([^&]*)")
internal fun Element.getLinkInformation(): LinkInformation? {
if (this.tagName() != "a") return null
return LinkInformation(this.text(), this.attr("href"))
}
internal fun URL.queryParams(): HashMap<String, MutableList<String>> {
val matches = queryStringRegex.findAll(this.query)
val map: HashMap<String, MutableList<String>> = hashMapOf()
for (match: MatchResult in matches) {
val (_, name, value) = match.groupValues
map.getOrPut(name) { mutableListOf() }.add(value)
}
return map
}