Desktop app (#308)
* Implement build profiles and feature policy for offline desktop builds * Introduce unified cross-platform Settings Hub * Refactor main settings into a hierarchical page-based navigation model * Refactor library projection to use shared multiplatform logic * Refactor UI state consumption by removing intermediate screen models * Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic * Refactor state management for tabs, selection, and pinning to use shared bridge logic * Refactor file type management and validation into a centralized shared module * Centralize file type resolution and improve handling of unknown types * Centralize book import logic with SharedImportPlanner * Refactor magnifier geometry logic and coordinate mapping * Properly handle orientation changes in scroll-locked PDF reader * Add screen orientation controls to EPUB and PDF readers * Implement right-to-left (RTL) pagination support and refactor reader menus * Separate right-to-left pagination settings for PDF and EPUB * Ensure PDF page data is scoped by document key for multi tab support * Implement theme-aware link styling for the epub reader * Implement jump history for back and forward navigation in the epub reader * Improve locator handling and navigation logic in paginated reader mode * Implement stable pagination navigation and location tracking * Centralize banner message management and auto-dismiss logic in MainViewModel * Implement zoom and pan state preservation for PDF pan lock mode * Enhance reader navigation UI and workspace layout management in desktop app * Refactor reader navigation sidebar and relocate search controls in desktop app * Enhance reader UI with redesigned selection menus and bottom sheet overlays * Implement custom highlight palettes and reader theme customization in desktop app * Implement cross-platform modal layer and refine reader UI styling * Improve highlight accuracy and implement metadata enrichment on book open in desktop app * Implement two-page spread layout for paginated reader on desktop * Implement persistent caching for book loading and pagination in desktop app * Implement persistent caching for book loading and pagination in desktop app * Optimize reader settings updates by separating layout and appearance changes in desktop app * Improve desktop window branding and native Windows styling * Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app * Refine selection handle positioning and interaction logic * Implement EPUB selection debug logging and improve handle targeting * Optimize desktop book loading performance and UI responsiveness * Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer. * Implement smooth zoom preview for the PDF reader in desktop app * Optimize PDF rendering performance and responsiveness in the desktop reader * Implement conditional diagnostic logging and update desktop build configuration * Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling * Added management options for annotations and highlights in the sidebar in desktop app * Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability. * Refined library filters and enhanced OPDS functionality in desktop app * Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app * Added PPTX support including document parsing, rendering, and indexing * Improved PPTX rendering and layout accuracy * Implemented text autofit support for PPTX rendering * Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity * Improved EPUB pagination accuracy and added layout telemetry in desktop app * Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app * Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling * Implemented billing account obfuscation * Implemented hierarchical folder navigation and improved library selection functionality in desktop app * Implemented platform-aware directory resolution and multi-platform native library support for desktop * Added full-screen mode for the reader workspace * Added PDF zoom indicator and interactive vertical scrollbar with page tooltips * Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management * Updated PDF indexing to replace existing page text and removed search result item keys * Implemented "preparing" foreground notification for TTS service * Optimized PDF rendering performance by pre-calculating page-specific annotations * Refactored desktop packaging tasks and improved distribution configuration * Optimized EPUB parser memory usage and added path traversal protection * Refactored WorkManager monitoring logic and added work pruning * Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks * Implemented bitmap size limits and scaling to prevent canvas rendering errors * Split long text paragraphs into multiple semantic blocks during HTML parsing * Implemented local ActionMode for text selection to prevent platform crashes * Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic * Added desktop startup splash screen and deferred WebView initialization * Reorganized settings hub and added separate PDF reader defaults * Implemented embedded cover extraction and metadata support for MOBI and FB2 formats * Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance. * Implemented procedurally generated book covers and replaced static placeholders * Redesigned search UI with a top bar and results overlay in desktop app * Added PDF page gap and overlay visibility options and implemented DesktopBookImporter * Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop * Implemented PDF viewport persistence for zoom and scroll positions in desktop app * Improved desktop fullscreen implementation and state restoration * Implemented desktop window state persistence * Implemented flavor-based branding and ProGuard configuration for desktop builds * Implemented precise reader positioning and improved highlight rendering logic in desktop app * Added support for user-editable book metadata * Enhanced book metadata support and integrated info/edit dialogs * Implemented embedded EPUB metadata editing * Improved highlight mapping and added custom scrollbar styling for the reader. * Reduced desktop WebView bundle size by excluding unused locales and runtime files * Added neutral pan mode as the default PDF interaction state. * Refactored library empty states and updated primary navigation tabs in desktop app * Implemented native paginated reader and unified content rendering architecture in desktop epub reader * Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing. * Improved pagination overflow detection in desktop * Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
This commit is contained in:
parent
c0d0e57e79
commit
b20ade9946
247 changed files with 43321 additions and 7087 deletions
|
|
@ -36,8 +36,40 @@ import org.jsoup.nodes.Element
|
|||
import org.jsoup.nodes.Node
|
||||
import org.jsoup.nodes.TextNode
|
||||
import org.jsoup.select.Selector
|
||||
import java.util.ArrayDeque
|
||||
import java.util.IdentityHashMap
|
||||
|
||||
private val unsupportedPseudoElementRegex = Regex("::?(before|after|first-letter|first-line|marker|selection)", RegexOption.IGNORE_CASE)
|
||||
private const val MAX_SEMANTIC_TEXT_BLOCK_CHARS = 32_000
|
||||
private const val TEXT_APPEND_SLICE_CHARS = 2_048
|
||||
private val semanticBlockDescendantTags = setOf(
|
||||
"img",
|
||||
"svg",
|
||||
"math-placeholder",
|
||||
"table",
|
||||
"hr",
|
||||
"div",
|
||||
"p",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"blockquote",
|
||||
"figure",
|
||||
"article",
|
||||
"aside",
|
||||
"header",
|
||||
"footer",
|
||||
"nav",
|
||||
"section",
|
||||
"main"
|
||||
)
|
||||
private val forcedStandaloneSemanticTags = setOf("img", "svg", "math-placeholder", "hr", "table")
|
||||
|
||||
interface HtmlResourceResolver {
|
||||
fun resolvePath(chapterAbsPath: String, extractionBasePath: String, src: String): String?
|
||||
|
|
@ -91,6 +123,12 @@ private fun String.capitalizeWords(): String =
|
|||
if (word.isNotEmpty()) word.replaceFirstChar { it.titlecase() } else ""
|
||||
}
|
||||
|
||||
private data class SemanticTextChunk(
|
||||
val text: String,
|
||||
val spans: List<SemanticSpan>,
|
||||
val startCharOffsetInSource: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* The public entry point for converting HTML to a list of [SemanticBlock]s.
|
||||
* This function sets up a parsing context and delegates the work to a [SemanticHtmlParser] instance.
|
||||
|
|
@ -144,13 +182,14 @@ private class SemanticHtmlParser(
|
|||
private val adaptThemeColors: Boolean
|
||||
) {
|
||||
private val styleCache = mutableMapOf<String, CssStyle>()
|
||||
private val semanticBlockDescendantCache = IdentityHashMap<Element, Boolean>()
|
||||
private var combinedRules: OptimizedCssRules = cssRules
|
||||
private val currentFontFamilyMap: MutableMap<String, FontFamily> = fontFamilyMap.toMutableMap()
|
||||
private var nextBlockIndex = 0
|
||||
|
||||
fun parse(html: String): List<SemanticBlock> {
|
||||
val document = Jsoup.parse(html, chapterAbsPath)
|
||||
val inlineCssContent = document.head().select("style").joinToString(separator = "\n") { it.data() }
|
||||
val inlineCssContent = document.head().getElementsByTag("style").joinToString(separator = "\n") { it.data() }
|
||||
|
||||
if (inlineCssContent.isNotBlank()) {
|
||||
HtmlParserLog.d("Found inline <style> content in $chapterAbsPath. Parsing...")
|
||||
|
|
@ -177,6 +216,61 @@ private class SemanticHtmlParser(
|
|||
return parseContainer(body, getElementStyle(body))
|
||||
}
|
||||
|
||||
private inline fun Element.anyChildElement(predicate: (Element) -> Boolean): Boolean {
|
||||
childNodes().forEach { child ->
|
||||
if (child is Element && predicate(child)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun Element.hasSemanticBlockDescendant(): Boolean {
|
||||
semanticBlockDescendantCache[this]?.let { return it }
|
||||
|
||||
if (anyChildElement { child -> child.tagName().lowercase() in semanticBlockDescendantTags }) {
|
||||
semanticBlockDescendantCache[this] = true
|
||||
return true
|
||||
}
|
||||
|
||||
val stack = ArrayDeque<Element>()
|
||||
stack.add(this)
|
||||
val expanded = IdentityHashMap<Element, Boolean>()
|
||||
|
||||
while (stack.isNotEmpty()) {
|
||||
val current = stack.peekLast()
|
||||
if (semanticBlockDescendantCache.containsKey(current)) {
|
||||
stack.removeLast()
|
||||
continue
|
||||
}
|
||||
|
||||
if (expanded.put(current, true) == null) {
|
||||
current.childNodes().forEach { child ->
|
||||
if (child is Element && !semanticBlockDescendantCache.containsKey(child)) {
|
||||
stack.add(child)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
stack.removeLast()
|
||||
val hasSemanticDescendant = current.anyChildElement { child ->
|
||||
child.tagName().lowercase() in semanticBlockDescendantTags ||
|
||||
semanticBlockDescendantCache[child] == true
|
||||
}
|
||||
semanticBlockDescendantCache[current] = hasSemanticDescendant
|
||||
}
|
||||
|
||||
return semanticBlockDescendantCache[this] == true
|
||||
}
|
||||
|
||||
private fun Element.isEffectivelySemanticBlock(): Boolean {
|
||||
val tagName = tagName().lowercase()
|
||||
return isBlock ||
|
||||
tagName in forcedStandaloneSemanticTags ||
|
||||
(!isBlock && hasSemanticBlockDescendant())
|
||||
}
|
||||
|
||||
private fun parseNodeToSemanticBlocks(
|
||||
element: Element,
|
||||
inheritedStyle: CssStyle
|
||||
|
|
@ -322,7 +416,7 @@ private class SemanticHtmlParser(
|
|||
"math-placeholder" -> parseMathPlaceholderToSemantic(element, elementStyle)
|
||||
"img" -> parseImageElementToSemantic(element, elementStyle)?.let { listOf(it) } ?: emptyList()
|
||||
"h1", "h2", "h3", "h4", "h5", "h6" -> {
|
||||
val hasNonTextChildren = element.select("img, svg, math-placeholder, table, hr, div, p, h1, h2, h3, h4, h5, h6, ul, ol, li, blockquote, figure, article, aside, header, footer, nav, section, main").isNotEmpty()
|
||||
val hasNonTextChildren = element.hasSemanticBlockDescendant()
|
||||
if (hasNonTextChildren) {
|
||||
val level = tagName.substring(1).toIntOrNull() ?: 1
|
||||
val fontSizeMultiplier = when (level) {
|
||||
|
|
@ -371,7 +465,7 @@ private class SemanticHtmlParser(
|
|||
"hr" -> listOf(SemanticSpacer(style = elementStyle, elementId = elementId, cfi = cfi, blockIndex = nextBlockIndex++))
|
||||
"ul", "ol" -> parseListElementToSemantic(element, elementStyle)
|
||||
else -> {
|
||||
val hasBlockDescendant = !element.isBlock && element.select("img, svg, math-placeholder, hr, table, div, p, h1, h2, h3, h4, h5, h6, ul, ol, li, blockquote, figure, article, aside, header, footer, nav, section, main").isNotEmpty()
|
||||
val hasBlockDescendant = !element.isBlock && element.hasSemanticBlockDescendant()
|
||||
if (element.isBlock || hasBlockDescendant) {
|
||||
parseContainer(element, elementStyle)
|
||||
} else {
|
||||
|
|
@ -397,32 +491,44 @@ private class SemanticHtmlParser(
|
|||
|
||||
fun flushTextBuffer() {
|
||||
if (textNodesBuffer.isEmpty()) return
|
||||
val (text, spans) = buildSemanticTextAndSpansFromNodes(textNodesBuffer, style)
|
||||
if (text.isNotBlank()) {
|
||||
val finalSpans = spans.toMutableList()
|
||||
val textChunks = buildSemanticTextAndSpanChunksFromNodes(textNodesBuffer, style)
|
||||
val containerElementId = element.id().ifBlank { null }
|
||||
val containerCfi = element.getCfiPath()
|
||||
textChunks.forEachIndexed { chunkIndex, chunk ->
|
||||
if (chunk.text.isBlank()) return@forEachIndexed
|
||||
|
||||
val finalSpans = chunk.spans.toMutableList()
|
||||
if (element.tagName().lowercase() == "a") {
|
||||
val href = element.attr("href").ifBlank { null }
|
||||
if (href != null) {
|
||||
finalSpans.add(SemanticSpan(
|
||||
start = 0,
|
||||
end = text.length,
|
||||
end = chunk.text.length,
|
||||
style = style,
|
||||
linkHref = href,
|
||||
tag = "a",
|
||||
elementId = element.id().ifBlank { null }
|
||||
elementId = containerElementId.takeIf { chunkIndex == 0 }
|
||||
))
|
||||
}
|
||||
}
|
||||
children.add(SemanticParagraph(text, finalSpans, style, element.id().ifBlank { null }, element.getCfiPath(), blockIndex = nextBlockIndex++))
|
||||
children.add(
|
||||
SemanticParagraph(
|
||||
text = chunk.text,
|
||||
spans = finalSpans,
|
||||
style = style,
|
||||
elementId = containerElementId.takeIf { chunkIndex == 0 },
|
||||
cfi = containerCfi,
|
||||
startCharOffsetInSource = chunk.startCharOffsetInSource,
|
||||
blockIndex = nextBlockIndex++
|
||||
)
|
||||
)
|
||||
}
|
||||
textNodesBuffer.clear()
|
||||
}
|
||||
|
||||
element.childNodes().forEach { node ->
|
||||
if (node is Element) {
|
||||
val tagName = node.tagName().lowercase()
|
||||
val isEffectivelyBlock = node.isBlock || tagName in listOf("img", "svg", "math-placeholder", "hr") ||
|
||||
(!node.isBlock && node.select("img, svg, math-placeholder, hr, table, div, p, h1, h2, h3, h4, h5, h6, ul, ol, li, blockquote, figure, article, aside, header, footer, nav, section, main").isNotEmpty())
|
||||
val isEffectivelyBlock = node.isEffectivelySemanticBlock()
|
||||
|
||||
if (isEffectivelyBlock) {
|
||||
flushTextBuffer()
|
||||
|
|
@ -450,79 +556,190 @@ private class SemanticHtmlParser(
|
|||
nodes: List<Node>,
|
||||
rootStyle: CssStyle
|
||||
): Pair<String, List<SemanticSpan>> {
|
||||
val chunks = buildSemanticTextAndSpanChunksFromNodes(nodes, rootStyle)
|
||||
val firstChunk = chunks.firstOrNull() ?: return "" to emptyList()
|
||||
return firstChunk.text to firstChunk.spans
|
||||
}
|
||||
|
||||
private fun buildSemanticTextAndSpanChunksFromNodes(
|
||||
nodes: List<Node>,
|
||||
rootStyle: CssStyle
|
||||
): List<SemanticTextChunk> {
|
||||
val textBuilder = StringBuilder()
|
||||
val spans = mutableListOf<SemanticSpan>()
|
||||
val chunks = mutableListOf<SemanticTextChunk>()
|
||||
val activeSpans = mutableListOf<ActiveSemanticSpan>()
|
||||
var currentChunkStartOffset = 0
|
||||
|
||||
fun processNode(node: Node, inheritedStyle: CssStyle) {
|
||||
when (node) {
|
||||
is TextNode -> {
|
||||
var text = node.wholeText.replace('\n', ' ')
|
||||
when (inheritedStyle.textTransform) {
|
||||
"uppercase" -> text = text.uppercase()
|
||||
"lowercase" -> text = text.lowercase()
|
||||
"capitalize" -> text = text.capitalizeWords()
|
||||
}
|
||||
textBuilder.append(text)
|
||||
}
|
||||
is Element -> {
|
||||
if (node.tagName().lowercase() == "br") {
|
||||
textBuilder.append('\n'); return
|
||||
}
|
||||
val currentElementStyle = getElementStyle(node)
|
||||
val newStyle = inheritedStyle.merge(currentElementStyle)
|
||||
val startIndex = textBuilder.length
|
||||
node.childNodes().forEach { processNode(it, newStyle) }
|
||||
val endIndex = textBuilder.length
|
||||
|
||||
val elementId = node.id().ifBlank { null }
|
||||
val isAnchor = node.tagName().lowercase() == "a" || elementId != null
|
||||
|
||||
// Capture span if it has content OR if it has an ID (anchor)
|
||||
if (startIndex < endIndex || elementId != null) {
|
||||
val href = if (node.tagName().lowercase() == "a") node.attr("href").ifBlank { null } else null
|
||||
spans.add(SemanticSpan(
|
||||
start = startIndex,
|
||||
end = endIndex,
|
||||
style = newStyle,
|
||||
linkHref = href,
|
||||
tag = node.tagName().lowercase(),
|
||||
elementId = elementId // Pass the ID here
|
||||
))
|
||||
}
|
||||
}
|
||||
fun addSpan(
|
||||
start: Int,
|
||||
end: Int,
|
||||
style: CssStyle,
|
||||
linkHref: String?,
|
||||
tag: String,
|
||||
elementId: String?
|
||||
) {
|
||||
if (start < end || elementId != null) {
|
||||
spans.add(
|
||||
SemanticSpan(
|
||||
start = start.coerceAtLeast(0),
|
||||
end = end.coerceAtLeast(start),
|
||||
style = style,
|
||||
linkHref = linkHref,
|
||||
tag = tag,
|
||||
elementId = elementId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
nodes.forEach { processNode(it, rootStyle) }
|
||||
|
||||
var processedText = textBuilder.toString()
|
||||
if (processedText.isNotEmpty() && processedText.last().isWhitespace()) {
|
||||
// 1. Find the index where trailing whitespace begins
|
||||
var newLength = processedText.length
|
||||
while (newLength > 0 && processedText[newLength - 1].isWhitespace()) {
|
||||
fun trimTrailingWhitespace(
|
||||
text: String,
|
||||
sourceSpans: List<SemanticSpan>
|
||||
): Pair<String, List<SemanticSpan>> {
|
||||
var newLength = text.length
|
||||
while (newLength > 0 && text[newLength - 1].isWhitespace()) {
|
||||
newLength--
|
||||
}
|
||||
|
||||
// 2. Cut the text
|
||||
processedText = processedText.substring(0, newLength)
|
||||
if (newLength == text.length) return text to sourceSpans
|
||||
|
||||
// 3. Filter or Cap spans so they don't point to indices that no longer exist
|
||||
val adjustedSpans = spans.mapNotNull { span ->
|
||||
val adjustedSpans = sourceSpans.mapNotNull { span ->
|
||||
if (span.start >= newLength) {
|
||||
// Span started in the whitespace area, remove it
|
||||
null
|
||||
} else if (span.end > newLength) {
|
||||
// Span ended in the whitespace area, cap it
|
||||
span.copy(end = newLength)
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
return processedText to adjustedSpans
|
||||
return text.substring(0, newLength) to adjustedSpans
|
||||
}
|
||||
|
||||
return processedText to spans
|
||||
fun flushChunk(trimTrailing: Boolean) {
|
||||
if (textBuilder.isEmpty()) return
|
||||
|
||||
activeSpans.forEach { active ->
|
||||
addSpan(
|
||||
start = active.startInChunk,
|
||||
end = textBuilder.length,
|
||||
style = active.style,
|
||||
linkHref = active.linkHref,
|
||||
tag = active.tag,
|
||||
elementId = active.elementId
|
||||
)
|
||||
}
|
||||
|
||||
val rawText = textBuilder.toString()
|
||||
val rawLength = rawText.length
|
||||
val (trimmedText, trimmedSpans) = if (trimTrailing) {
|
||||
trimTrailingWhitespace(rawText, spans)
|
||||
} else {
|
||||
rawText to spans.toList()
|
||||
}
|
||||
if (trimmedText.isNotBlank()) {
|
||||
chunks.add(
|
||||
SemanticTextChunk(
|
||||
text = trimmedText,
|
||||
spans = trimmedSpans,
|
||||
startCharOffsetInSource = currentChunkStartOffset
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
currentChunkStartOffset += rawLength
|
||||
textBuilder.clear()
|
||||
spans.clear()
|
||||
activeSpans.forEach { it.startInChunk = 0 }
|
||||
}
|
||||
|
||||
fun appendText(text: String) {
|
||||
var offset = 0
|
||||
while (offset < text.length) {
|
||||
if (textBuilder.length >= MAX_SEMANTIC_TEXT_BLOCK_CHARS) {
|
||||
flushChunk(trimTrailing = false)
|
||||
}
|
||||
val available = (MAX_SEMANTIC_TEXT_BLOCK_CHARS - textBuilder.length).coerceAtLeast(1)
|
||||
val end = (offset + available).coerceAtMost(text.length)
|
||||
textBuilder.append(text, offset, end)
|
||||
offset = end
|
||||
if (textBuilder.length >= MAX_SEMANTIC_TEXT_BLOCK_CHARS) {
|
||||
flushChunk(trimTrailing = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun appendTransformedText(rawText: String, textTransform: String?) {
|
||||
var start = 0
|
||||
while (start < rawText.length) {
|
||||
val end = (start + TEXT_APPEND_SLICE_CHARS).coerceAtMost(rawText.length)
|
||||
val normalizedSlice = buildString(end - start) {
|
||||
for (i in start until end) {
|
||||
append(if (rawText[i] == '\n') ' ' else rawText[i])
|
||||
}
|
||||
}
|
||||
val transformedSlice = when (textTransform) {
|
||||
"uppercase" -> normalizedSlice.uppercase()
|
||||
"lowercase" -> normalizedSlice.lowercase()
|
||||
"capitalize" -> normalizedSlice.capitalizeWords()
|
||||
else -> normalizedSlice
|
||||
}
|
||||
appendText(transformedSlice)
|
||||
start = end
|
||||
}
|
||||
}
|
||||
|
||||
fun processNode(node: Node, inheritedStyle: CssStyle) {
|
||||
when (node) {
|
||||
is TextNode -> {
|
||||
appendTransformedText(node.wholeText, inheritedStyle.textTransform)
|
||||
}
|
||||
is Element -> {
|
||||
if (node.tagName().lowercase() == "br") {
|
||||
appendText("\n"); return
|
||||
}
|
||||
val currentElementStyle = getElementStyle(node)
|
||||
val newStyle = inheritedStyle.merge(currentElementStyle)
|
||||
val tag = node.tagName().lowercase()
|
||||
val href = if (tag == "a") node.attr("href").ifBlank { null } else null
|
||||
val elementId = node.id().ifBlank { null }
|
||||
val activeSpan = ActiveSemanticSpan(
|
||||
startInChunk = textBuilder.length,
|
||||
style = newStyle,
|
||||
linkHref = href,
|
||||
tag = tag,
|
||||
elementId = elementId
|
||||
)
|
||||
activeSpans.add(activeSpan)
|
||||
node.childNodes().forEach { processNode(it, newStyle) }
|
||||
activeSpans.removeAt(activeSpans.lastIndex)
|
||||
val endIndex = textBuilder.length
|
||||
|
||||
// Capture span if it has content OR if it has an ID (anchor)
|
||||
addSpan(
|
||||
start = activeSpan.startInChunk,
|
||||
end = endIndex,
|
||||
style = newStyle,
|
||||
linkHref = href,
|
||||
tag = tag,
|
||||
elementId = elementId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
nodes.forEach { processNode(it, rootStyle) }
|
||||
flushChunk(trimTrailing = true)
|
||||
return chunks
|
||||
}
|
||||
|
||||
private data class ActiveSemanticSpan(
|
||||
var startInChunk: Int,
|
||||
val style: CssStyle,
|
||||
val linkHref: String?,
|
||||
val tag: String,
|
||||
val elementId: String?
|
||||
)
|
||||
|
||||
private fun parseMathPlaceholderToSemantic(element: Element, style: CssStyle): List<SemanticBlock> {
|
||||
val uniqueId = element.id()
|
||||
val svgContent = mathSvgCache[uniqueId]
|
||||
|
|
@ -532,7 +749,7 @@ private class SemanticHtmlParser(
|
|||
var svgViewBox: String? = null
|
||||
if (svgContent != null) {
|
||||
val svgDoc = Jsoup.parse(svgContent)
|
||||
svgDoc.selectFirst("svg")?.let {
|
||||
svgDoc.getElementsByTag("svg").firstOrNull()?.let {
|
||||
svgWidth = it.attr("width")
|
||||
svgHeight = it.attr("height")
|
||||
svgViewBox = it.attr("viewBox")
|
||||
|
|
@ -564,7 +781,7 @@ private class SemanticHtmlParser(
|
|||
|
||||
return SemanticImage(
|
||||
path = imagePath,
|
||||
altText = svgElement.selectFirst("title")?.text() ?: "Cover Image",
|
||||
altText = svgElement.getElementsByTag("title").firstOrNull()?.text() ?: "Cover Image",
|
||||
intrinsicWidth = width,
|
||||
intrinsicHeight = height,
|
||||
style = style,
|
||||
|
|
@ -575,8 +792,8 @@ private class SemanticHtmlParser(
|
|||
}
|
||||
|
||||
HtmlParserLog.d("Parsing genuine SVG content into SemanticMath block.")
|
||||
val title = svgElement.selectFirst("title")?.text()
|
||||
val desc = svgElement.selectFirst("desc")?.text()
|
||||
val title = svgElement.getElementsByTag("title").firstOrNull()?.text()
|
||||
val desc = svgElement.getElementsByTag("desc").firstOrNull()?.text()
|
||||
val altText = title ?: desc ?: "SVG Image"
|
||||
|
||||
return SemanticMath(
|
||||
|
|
@ -644,7 +861,7 @@ private class SemanticHtmlParser(
|
|||
}
|
||||
|
||||
private fun parseTableElementToSemantic(tableElement: Element, tableStyle: CssStyle): SemanticTable? {
|
||||
val rows = tableElement.select("tr").mapNotNull { rowElement ->
|
||||
val rows = tableElement.getElementsByTag("tr").mapNotNull { rowElement ->
|
||||
val rowStyle = getElementStyle(rowElement)
|
||||
if (rowStyle.display == "none") return@mapNotNull null
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue