Txt file fix (#35)

* Fixed virtualization data indexing and improved text file to EPUB conversion logic.

- Corrected array indexing for `chunksData` and `chunkHeights` in `epub_reader.js` to prevent overwriting entire arrays.
- Refined virtualization intersection observer to properly restore and unload chunk content using index-based caching.
- Updated `SingleFileImporter.kt` to convert plain text into structured HTML paragraphs instead of a single `pre` tag.
- Enhanced CSS styles for imported single-file documents and added HTML entity escaping.

* Adjusted text layout and enhanced pagination mismatch logging

*   Configured `PlatformTextStyle` and `LineHeightStyle` to improve text measurement consistency.
*   Updated `checkLayoutMismatch` to include text snippets and handle zero expected height.
*   Refined `Modifier` application order in the reader view.
*   Ensured `maxHeight` is set to `Infinity` during text measurement in `Paginator`.
This commit is contained in:
Aryan 2026-03-06 14:06:29 +05:30 committed by GitHub
parent 506f60473c
commit 8f52549c19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 98 additions and 63 deletions

View file

@ -1820,15 +1820,7 @@
observer: null,
init: function (initialChunkIndex, total) {
console.log(`Virtualization: Init with $ {
total
}
chunks. Anchor: $ {
initialChunkIndex
}
`);
console.log(`Virtualization: Init with ${total} chunks. Anchor: ${initialChunkIndex}`);
this.totalChunks = total;
this.chunksData = new Array(total).fill(null);
this.chunkHeights = new Array(total).fill(0);
@ -1838,11 +1830,12 @@
if (container) {
container.querySelectorAll(".chunk-container").forEach((div) => {
let idx = parseInt(div.dataset.chunkIndex, 10);
let content = div.innerHTML.trim();
let content = div.innerHTML; // Don't trim, keep original HTML structure
if (content.length > 0) {
this.chunksData = content;
this.chunkHeights = div.getBoundingClientRect().height;
if (content.trim().length > 0) {
// FIX: Assign to specific index, don't overwrite the whole array
this.chunksData[idx] = content;
this.chunkHeights[idx] = div.getBoundingClientRect().height;
}
});
}
@ -1862,26 +1855,31 @@
let idx = parseInt(div.dataset.chunkIndex, 10);
if (entry.isIntersecting) {
if (!this.chunksData) {
// Check if we have data for this chunk at specific index
if (!this.chunksData[idx]) {
if (window.ContentBridge && window.ContentBridge.requestChunk) {
window.ContentBridge.requestChunk(idx);
}
} else if (div.innerHTML === "") {
// Restore content from cache
let oldHeight = div.getBoundingClientRect().height;
div.innerHTML = this.chunksData;
div.style.height = "";
let newHeight = div.getBoundingClientRect().height;
this.chunkHeights = newHeight;
div.innerHTML = this.chunksData[idx]; // FIX: Access by index
div.style.height = ""; // Allow auto height
let newHeight = div.getBoundingClientRect().height;
this.chunkHeights[idx] = newHeight; // Update cached height
// Adjust scroll if this expansion happened above our viewport
if (div.getBoundingClientRect().top < 0) {
scrollAdjust += newHeight - oldHeight;
scrollAdjust += (newHeight - oldHeight);
}
}
} else {
// Unload content to save memory/DOM weight
if (div.innerHTML !== "") {
let oldHeight = div.getBoundingClientRect().height;
this.chunkHeights = oldHeight;
div.style.height = oldHeight + "px";
this.chunkHeights[idx] = oldHeight;
div.style.height = oldHeight + "px"; // Fix height to placeholder
div.innerHTML = "";
}
}
@ -1891,8 +1889,7 @@
window.scrollBy(0, scrollAdjust);
}
},
{ rootMargin: "2500px 0px" },
{ rootMargin: "2500px 0px" } // Keep large margin for smooth scrolling
);
document.querySelectorAll(".chunk-container").forEach((div) => {

View file

@ -105,17 +105,8 @@ class SingleFileImporter(private val context: Context) {
var chapterCounter = 1
val cssStyle = """
body { margin: 0; padding: 0; }
pre {
font-family: 'Courier New', Courier, monospace;
font-size: 1em;
line-height: 1.5;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
padding: 1em;
margin: 0;
}
body { font-family: sans-serif; line-height: 1.6; padding: 1em; max-width: 800px; margin: 0 auto; }
p { margin-bottom: 1em; text-indent: 1.5em; }
""".trimIndent()
val currentChapterContent = StringBuilder()
@ -135,7 +126,9 @@ class SingleFileImporter(private val context: Context) {
<title>$chapterTitle</title>
<style>$cssStyle</style>
</head>
<body><pre>${currentChapterContent}</pre></body>
<body>
$currentChapterContent
</body>
</html>
""".trimIndent()
@ -160,37 +153,55 @@ class SingleFileImporter(private val context: Context) {
chapterCounter++
}
fun escapeHtml(text: String): String {
return text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
}
val reader = inputStream.bufferedReader()
val buffer = CharArray(8192)
var inParagraph = false
while (true) {
val readCount = reader.read(buffer)
if (readCount == -1) break
for (i in 0 until readCount) {
val c = buffer[i]
if ((c < ' ' && c != '\t' && c != '\n' && c != '\r')) {
continue
}
when (c) {
'<' -> currentChapterContent.append("&lt;")
'>' -> currentChapterContent.append("&gt;")
'&' -> currentChapterContent.append("&amp;")
else -> currentChapterContent.append(c)
val line = reader.readLine()
if (line == null) {
if (inParagraph) {
currentChapterContent.append("</p>\n")
}
break
}
if (currentChapterContent.length >= chapterTargetSize) {
flushChapter()
val trimmed = line.trim()
if (trimmed.isEmpty()) {
if (inParagraph) {
currentChapterContent.append("</p>\n")
inParagraph = false
}
if (currentChapterContent.length >= chapterTargetSize) {
flushChapter()
}
} else {
if (!inParagraph) {
currentChapterContent.append("<p>")
inParagraph = true
} else {
currentChapterContent.append(" ")
}
currentChapterContent.append(escapeHtml(trimmed))
if (currentChapterContent.length >= chapterTargetSize * 2) {
currentChapterContent.append("</p>\n")
flushChapter()
inParagraph = false
}
}
}
flushChapter()
if (chapters.isEmpty()) {
currentChapterContent.append("(Empty File)")
currentChapterContent.append("<p>(Empty File)</p>")
flushChapter()
}

View file

@ -31,6 +31,8 @@ import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -544,7 +546,12 @@ fun PaginatedReaderScreen(
lineHeight = adjustedLineHeight,
fontFamily = debouncedFontFamily,
lineBreak = LineBreak.Paragraph,
letterSpacing = TextUnit.Unspecified
letterSpacing = TextUnit.Unspecified,
platformStyle = PlatformTextStyle(includeFontPadding = false),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Proportional,
trim = LineHeightStyle.Trim.None
)
)
}
@ -1302,15 +1309,22 @@ private fun checkLayoutMismatch(
blockType: String,
expectedHeight: Int,
actualHeight: Int,
tolerance: Int = 2
textSnippet: String,
@Suppress("SameParameterValue") tolerance: Int = 2
) {
if (expectedHeight == 0) {
Timber.tag("PAGINATION_MISMATCH").w("Block #$blockIndex ($blockType) has expectedHeight=0. Skipping check. Text: '$textSnippet'")
return
}
if (actualHeight > expectedHeight + tolerance) {
val diff = actualHeight - expectedHeight
Timber.tag("PAGINATION_MISMATCH").e(
"OVERFLOW DETECTED! Block #$blockIndex ($blockType)\n" +
" -> Expected: ${expectedHeight}px\n" +
" -> Actual: ${actualHeight}px\n" +
" -> Diff: +${diff}px"
" -> Diff: +${diff}px\n" +
" -> Content: '$textSnippet'"
)
}
}
@ -1560,8 +1574,7 @@ internal fun PaginatedReaderContent(
Modifier.fillMaxWidth()
}
val boxModifier = marginModifier
.then(alignModifier)
val styleModifier = alignModifier
.then(if (block.style.horizontalAlign == "center") widthModifier else Modifier)
.then(
if (block.style.borderRadius > 0.dp) Modifier.clip(RoundedCornerShape(block.style.borderRadius))
@ -1588,15 +1601,27 @@ internal fun PaginatedReaderContent(
.onGloballyPositioned { coordinates ->
val actualHeight = coordinates.size.height
if (block.expectedHeight > 0) {
val snippet = when(block) {
is ParagraphBlock -> block.content.text.take(50)
is HeaderBlock -> block.content.text.take(50)
is QuoteBlock -> block.content.text.take(50)
is ListItemBlock -> block.content.text.take(50)
is TextContentBlock -> block.content.text.take(50)
else -> "Non-text content"
}
checkLayoutMismatch(
blockIndex = block.blockIndex,
blockType = block::class.simpleName ?: "Block",
expectedHeight = block.expectedHeight,
actualHeight = actualHeight
actualHeight = actualHeight,
textSnippet = snippet,
tolerance = 2
)
}
}
.then(boxModifier)
.then(marginModifier)
.then(styleModifier)
Box(modifier = diagnosticModifier) {
val borderWidth = block.style.border?.width ?: 0.dp

View file

@ -710,7 +710,8 @@ private suspend fun measureBlockHeight(
}
val adjustedConstraints = constraints.copy(
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0)
maxWidth = contentMaxWidth.roundToInt().coerceAtLeast(0),
maxHeight = Constraints.Infinity
)
val contentHeight = when (block) {
@ -1021,7 +1022,7 @@ private suspend fun splitParagraphBlock(
textMeasurer.measure(
text = text,
style = textStyle,
constraints = constraints
constraints = constraints.copy(maxHeight = Constraints.Infinity)
)
}