()
+ var inMath = false
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ when (eventType) {
+ XmlPullParser.START_TAG -> {
+ val name = parser.name
+ if (inMath) {
+ if (name != "math:math") {
+ currentChapterHtml.append("<$name")
+ for (i in 0 until parser.attributeCount) {
+ val attrName = parser.getAttributeName(i)
+ val attrValue = parser.getAttributeValue(i)?.replace("\"", """)
+ currentChapterHtml.append(" $attrName=\"$attrValue\"")
+ }
+ currentChapterHtml.append(">")
+ }
+ } else {
+ when (name) {
+ "dc:title" -> {
+ val t = parser.nextText().trim()
+ if (t.isNotBlank()) title = t
+ }
+ "dc:creator" -> {
+ val a = parser.nextText().trim()
+ if (a.isNotBlank()) author = a
+ }
+ "style:style" -> {
+ currentParsedStyleName = parser.getAttributeValue(null, "style:name")
+ if (currentParsedStyleName != null) {
+ styleMap[currentParsedStyleName] = StyleProps()
+ }
+ }
+ "style:text-properties" -> {
+ val props = styleMap[currentParsedStyleName]
+ if (props != null) {
+ val weight = parser.getAttributeValue(null, "fo:font-weight")
+ if (weight == "bold") props.isBold = true
+
+ val style = parser.getAttributeValue(null, "fo:font-style")
+ if (style == "italic") props.isItalic = true
+
+ val lineThrough = parser.getAttributeValue(null, "style:text-line-through-style")
+ if (lineThrough != null && lineThrough != "none") props.isStrikethrough = true
+
+ val underline = parser.getAttributeValue(null, "style:text-underline-style")
+ if (underline != null && underline != "none") props.isUnderline = true
+ }
+ }
+ "text:h" -> {
+ val levelStr = parser.getAttributeValue(null, "text:outline-level")
+ val level = levelStr?.toIntOrNull() ?: 2
+ val hTag = "h${level.coerceIn(1, 6)}"
+ headerStack.addLast(hTag)
+ currentChapterHtml.append("<$hTag>")
+ }
+ "text:p" -> currentChapterHtml.append("")
+ "text:span" -> {
+ val styleName = parser.getAttributeValue(null, "text:style-name")
+ val props = styleMap[styleName]
+ val openedTags = mutableListOf()
+ if (props != null) {
+ if (props.isBold) { currentChapterHtml.append(""); openedTags.add("b") }
+ if (props.isItalic) { currentChapterHtml.append(""); openedTags.add("i") }
+ if (props.isUnderline) { currentChapterHtml.append(""); openedTags.add("u") }
+ if (props.isStrikethrough) { currentChapterHtml.append(""); openedTags.add("s") }
+ }
+ spanStack.addLast(openedTags)
+ }
+ "text:a" -> {
+ val href = parser.getAttributeValue(null, "xlink:href") ?: ""
+ currentChapterHtml.append("")
+ }
+ "text:list" -> currentChapterHtml.append("\n")
+ "text:list-item" -> currentChapterHtml.append("- ")
+ "table:table" -> currentChapterHtml.append("
")
+ "table:table-row" -> currentChapterHtml.append("")
+ "table:table-cell" -> {
+ val colspan = parser.getAttributeValue(null, "table:number-columns-spanned") ?: "1"
+ val rowspan = parser.getAttributeValue(null, "table:number-rows-spanned") ?: "1"
+ currentChapterHtml.append("")
+ }
+ "text:line-break" -> currentChapterHtml.append(" ")
+ "text:tab" -> currentChapterHtml.append(" ")
+ "text:note" -> currentChapterHtml.append(" |
\n")
+ "table:table-row" -> currentChapterHtml.append("\n")
+ "table:table-cell" -> currentChapterHtml.append("\n")
+ "text:note" -> currentChapterHtml.append("]")
+ "office:binary-data" -> {
+ inOfficeBinaryData = false
+ if (isFlat) {
+ try {
+ val bytes = Base64.decode(base64Builder.toString(), Base64.DEFAULT)
+ val imgName = currentImageHref?.substringAfterLast("/") ?: "${UUID.randomUUID()}.png"
+ val imgFile = File(extractionDir, imgName)
+ FileOutputStream(imgFile).use { it.write(bytes) }
+ currentChapterHtml.append("
")
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to decode FODT image")
+ }
+ currentImageHref = null
+ }
+ }
+ }
+ }
+ }
+ }
+ if (eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next()
+ }
+ }
+
+ saveChapter()
+
+ if (chapters.isEmpty() && parseContent) {
+ if (currentChapterHtml.isNotBlank()) {
+ saveChapter()
+ } else {
+ throw Exception("No valid content found in ODT file.")
+ }
+ }
+
+ val finalCoverBitmap = coverBytes?.let {
+ try {
+ BitmapFactory.decodeByteArray(it, 0, it.size)
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to decode thumbnail bitmap")
+ null
+ }
+ }
+
+ return@withContext EpubBook(
+ fileName = originalBookNameHint,
+ title = title,
+ author = author,
+ language = "en",
+ coverImage = finalCoverBitmap,
+ chapters = chapters,
+ chaptersForPagination = chapters,
+ images = images,
+ pageList = emptyList(),
+ extractionBasePath = extractionDir.absolutePath,
+ css = emptyMap()
+ )
+ } finally {
+ try {
+ inputStream.close()
+ } catch (e: Exception) {
+ Timber.e(e, "Error closing ODT stream")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt
index e8ea9b9..3c6f086 100644
--- a/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt
+++ b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt
@@ -40,6 +40,7 @@ import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
+import java.util.UUID
class SingleFileImporter(private val context: Context) {
@@ -52,6 +53,15 @@ class SingleFileImporter(private val context: Context) {
bookId: String,
parseContent: Boolean = true
): EpubBook {
+
+ val lowerHint = originalBookNameHint.lowercase()
+ val isCsv = lowerHint.endsWith(".csv") || lowerHint.endsWith(".tsv")
+ val isCodeOrData = listOf(".json", ".xml", ".log", ".java", ".kt", ".py", ".js", ".cpp", ".c", ".cs", ".rb", ".go").any { lowerHint.endsWith(it) }
+
+ if (type == FileType.HTML && (isCsv || isCodeOrData)) {
+ return parseDynamicContentToHtml(inputStream, originalBookNameHint, bookId, parseContent, isCsv)
+ }
+
return when (type) {
FileType.MD -> parseMarkdown(inputStream, originalBookNameHint, bookId, parseContent)
FileType.TXT -> parsePlainText(inputStream, originalBookNameHint, bookId, parseContent)
@@ -61,6 +71,79 @@ class SingleFileImporter(private val context: Context) {
}
}
+ private suspend fun parseDynamicContentToHtml(
+ inputStream: InputStream,
+ originalBookNameHint: String,
+ bookId: String,
+ parseContent: Boolean,
+ isCsv: Boolean
+ ): EpubBook = withContext(Dispatchers.IO) {
+
+ val tempFile = File(context.cacheDir, "temp_conv_${UUID.randomUUID()}.html")
+
+ try {
+ FileOutputStream(tempFile).bufferedWriter().use { writer ->
+ writer.write("\n\n\n\n${originalBookNameHint}\n")
+
+ if (isCsv) {
+ writer.write("\n")
+ writer.write("\n\n\n
\n")
+ } else {
+ writer.write("\n")
+ writer.write("\n\n\n")
+ }
+
+ val delimiter = if (originalBookNameHint.lowercase().endsWith(".tsv")) '\t' else ','
+
+ inputStream.bufferedReader().use { reader ->
+ var line = reader.readLine()
+ while (line != null) {
+ if (isCsv) {
+ writer.write("")
+ val current = StringBuilder()
+ var inQuotes = false
+
+ for (char in line) {
+ if (char == '\"') {
+ inQuotes = !inQuotes
+ } else if (char == delimiter && !inQuotes) {
+ val escaped = current.toString().replace("&", "&").replace("<", "<").replace(">", ">")
+ writer.write("| $escaped | ")
+ current.clear()
+ } else {
+ current.append(char)
+ }
+ }
+ val escapedFinal = current.toString().replace("&", "&").replace("<", "<").replace(">", ">")
+ writer.write("$escapedFinal |
\n")
+
+ } else {
+ // Plain code/log text escaping
+ val escaped = line.replace("&", "&").replace("<", "<").replace(">", ">")
+ writer.write("$escaped\n")
+ }
+ line = reader.readLine()
+ }
+ }
+
+ if (isCsv) {
+ writer.write("
\n
\n\n")
+ } else {
+ writer.write("\n