feat: Add Nitro EPUB Exporter

This commit is contained in:
Rajarshee Chatterjee
2026-07-24 12:30:43 +05:30
parent a421177f0c
commit 4cb1890b62
80 changed files with 1955 additions and 506 deletions
+1
View File
@@ -111,3 +111,4 @@ src/generated/**/*
android/
!modules/**/android/
ios/
!modules/**/ios/
@@ -3,7 +3,7 @@ require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "NitroImportEpub"
s.name = "NitroEpub"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
@@ -21,11 +21,13 @@ Pod::Spec.new do |s|
# Implementation (C++ objects)
"cpp/**/*.{hpp,cpp}",
]
s.exclude_files = "cpp/tests/**/*"
load 'nitrogen/generated/ios/NitroImportEpub+autolinking.rb'
load 'nitrogen/generated/ios/NitroEpub+autolinking.rb'
add_nitrogen_files(s)
s.dependency 'React-jsi'
s.dependency 'React-callinvoker'
s.library = 'z'
install_modules_dependencies(s)
end
+29
View File
@@ -0,0 +1,29 @@
# Nitro EPUB
Cross-platform C++ EPUB support for LNReader, exposed through React Native Nitro
Modules.
## Capabilities
- Parse an extracted EPUB into novel metadata and chapter paths.
- Export downloaded chapter files as an EPUB 3 archive.
- Copy local chapter images into the archive and remove missing images.
- Generate EPUB navigation, NCX compatibility navigation, metadata, CSS, and
optional JavaScript.
- Write the required `mimetype` entry first and without compression.
- Build exports on a Nitro worker thread and atomically publish the completed
archive.
The exporter accepts chapter file paths rather than chapter bodies so large
novels do not need to be retained in the JavaScript heap.
Import the shared runtime object from the package:
```ts
import { epub } from 'nitro-epub'
const novel = await epub.parseNovelAndChapters(extractedDirectory)
```
Run `pnpm specs` in this directory after changing `src/specs/Epub.nitro.ts`.
Generated Nitrogen sources are committed.
@@ -1,20 +1,24 @@
project(NitroImportEpub)
project(NitroEpub)
cmake_minimum_required(VERSION 3.9.0)
set (PACKAGE_NAME NitroImportEpub)
set (PACKAGE_NAME NitroEpub)
set (CMAKE_VERBOSE_MAKEFILE ON)
set (CMAKE_CXX_STANDARD 20)
# Define C++ library and add all sources
add_library(${PACKAGE_NAME} SHARED
src/main/cpp/cpp-adapter.cpp
../cpp/EpubParser.cpp
../cpp/import/EpubParser.cpp
../cpp/export/EpubContentProcessor.cpp
../cpp/export/EpubDocuments.cpp
../cpp/export/EpubExporter.cpp
../cpp/export/ZipWriter.cpp
../cpp/pugixml.cpp
../cpp/HybridEpub.cpp
)
# Add Nitrogen specs :)
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroImportEpub+autolinking.cmake)
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroEpub+autolinking.cmake)
# Set up local includes
include_directories(
@@ -23,10 +27,12 @@ include_directories(
)
find_library(LOG_LIB log)
find_library(Z_LIB z)
# Link all libraries together
target_link_libraries(
${PACKAGE_NAME}
${LOG_LIB}
${Z_LIB}
android # <-- Android core
)
@@ -20,7 +20,7 @@ def isNewArchitectureEnabled() {
apply plugin: "com.android.library"
apply plugin: 'org.jetbrains.kotlin.android'
apply from: '../nitrogen/generated/android/NitroImportEpub+autolinking.gradle'
apply from: '../nitrogen/generated/android/NitroEpub+autolinking.gradle'
apply from: "./fix-prefab.gradle"
if (isNewArchitectureEnabled()) {
@@ -28,15 +28,15 @@ if (isNewArchitectureEnabled()) {
}
def getExtOrDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroImportEpub_" + name]
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroEpub_" + name]
}
def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroImportEpub_" + name]).toInteger()
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroEpub_" + name]).toInteger()
}
android {
namespace "com.margelo.nitro.nitroimportepub"
namespace "com.margelo.nitro.nitroepub"
ndkVersion getExtOrDefault("ndkVersion")
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
@@ -139,4 +139,3 @@ dependencies {
// Add a dependency on NitroModules
implementation project(":react-native-nitro-modules")
}
@@ -0,0 +1,5 @@
NitroEpub_kotlinVersion=2.1.21
NitroEpub_minSdkVersion=23
NitroEpub_targetSdkVersion=36
NitroEpub_compileSdkVersion=36
NitroEpub_ndkVersion=29.0.14206865
@@ -1,9 +1,9 @@
#include <jni.h>
#include <fbjni/fbjni.h>
#include "NitroImportEpubOnLoad.hpp"
#include "NitroEpubOnLoad.hpp"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
return facebook::jni::initialize(vm, []() {
margelo::nitro::nitroimportepub::registerAllNatives();
margelo::nitro::nitroepub::registerAllNatives();
});
}
@@ -1,18 +1,18 @@
package com.margelo.nitro.nitroimportepub
package com.margelo.nitro.nitroepub
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.BaseReactPackage
class NitroImportEpubPackage : BaseReactPackage() {
class NitroEpubPackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { HashMap() }
companion object {
init {
NitroImportEpubOnLoad.initializeNative()
NitroEpubOnLoad.initializeNative()
}
}
}
+70
View File
@@ -0,0 +1,70 @@
#include "HybridEpub.hpp"
#include "export/EpubExporter.hpp"
#include "import/EpubParser.hpp"
namespace margelo::nitro::nitroepub {
std::shared_ptr<Promise<EpubNovel>> HybridEpub::parseNovelAndChapters(
const std::string& epubDirPath) {
return Promise<EpubNovel>::async([epubDirPath]() {
EpubMetadata metadata = parseEpub(epubDirPath);
EpubNovel result;
result.name = metadata.name;
result.cover = metadata.cover.empty()
? std::nullopt
: std::optional(metadata.cover);
result.summary = metadata.summary.empty()
? std::nullopt
: std::optional(metadata.summary);
result.author = metadata.author.empty()
? std::nullopt
: std::optional(metadata.author);
result.artist = metadata.artist.empty()
? std::nullopt
: std::optional(metadata.artist);
for (const auto& chapter : metadata.chapters) {
result.chapters.emplace_back(chapter.name, chapter.path);
}
result.cssPaths = metadata.cssPaths;
result.imagePaths = metadata.imagePaths;
return result;
});
}
std::shared_ptr<Promise<EpubExportResult>> HybridEpub::exportEpub(
const EpubExportMetadata& metadata,
const std::vector<EpubExportChapter>& chapters,
const std::string& outputPath) {
return Promise<EpubExportResult>::async(
[metadata, chapters, outputPath]() {
EpubArchiveMetadata archiveMetadata{
metadata.title,
metadata.language,
metadata.coverPath,
metadata.description,
metadata.author,
metadata.bookId,
metadata.stylesheet,
metadata.javascript,
};
std::vector<EpubArchiveChapter> archiveChapters;
archiveChapters.reserve(chapters.size());
for (const EpubExportChapter& chapter : chapters) {
archiveChapters.push_back(EpubArchiveChapter{
chapter.title,
chapter.htmlPath,
chapter.novelId,
chapter.chapterId,
});
}
const EpubArchiveResult result =
exportEpubArchive(archiveMetadata, archiveChapters, outputPath);
return EpubExportResult(
result.outputPath, static_cast<double>(result.chapterCount));
});
}
} // namespace margelo::nitro::nitroepub
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "HybridEpubSpec.hpp"
namespace margelo::nitro::nitroepub {
class HybridEpub final : public HybridEpubSpec {
public:
HybridEpub() : HybridObject(TAG) {}
~HybridEpub() override = default;
std::shared_ptr<Promise<EpubNovel>> parseNovelAndChapters(
const std::string& epubDirPath) override;
std::shared_ptr<Promise<EpubExportResult>> exportEpub(
const EpubExportMetadata& metadata,
const std::vector<EpubExportChapter>& chapters,
const std::string& outputPath) override;
};
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace margelo::nitro::nitroepub {
struct EpubAsset {
std::string id;
std::string archivePath;
std::string mediaType;
};
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,261 @@
#include "EpubContentProcessor.hpp"
#include "../pugixml.hpp"
#include <algorithm>
#include <cctype>
#include <fstream>
#include <regex>
#include <sstream>
#include <stdexcept>
#include <unordered_set>
namespace margelo::nitro::nitroepub {
namespace {
std::string readTextFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
throw std::runtime_error("Unable to read downloaded chapter " +
path.string());
}
return std::string(std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
std::string lowercase(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](char character) {
return static_cast<char>(
std::tolower(static_cast<unsigned char>(character)));
});
return value;
}
int hexValue(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
character = static_cast<char>(std::tolower(
static_cast<unsigned char>(character)));
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
return -1;
}
std::string percentDecode(const std::string& value) {
std::string decoded;
decoded.reserve(value.size());
for (std::size_t index = 0; index < value.size(); ++index) {
if (value[index] == '%' && index + 2 < value.size()) {
const int high = hexValue(value[index + 1]);
const int low = hexValue(value[index + 2]);
if (high >= 0 && low >= 0) {
decoded += static_cast<char>((high << 4) | low);
index += 2;
continue;
}
}
decoded += value[index];
}
return decoded;
}
std::string extractBody(std::string html) {
html = std::regex_replace(
html, std::regex(R"(<!doctype[^>]*>)", std::regex::icase), "");
const std::regex bodyPattern(
R"(<body\b[^>]*>([\s\S]*?)</body\s*>)",
std::regex::icase);
std::smatch match;
if (std::regex_search(html, match, bodyPattern)) {
return match[1].str();
}
return html;
}
std::string makeVoidElementsXmlCompatible(std::string html) {
const std::regex voidElement(
R"(<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)(\b[^<>]*?)(/?)>)",
std::regex::icase);
std::string result;
std::size_t cursor = 0;
for (std::sregex_iterator iterator(html.begin(), html.end(), voidElement),
end;
iterator != end; ++iterator) {
const auto& match = *iterator;
result.append(html, cursor, static_cast<std::size_t>(match.position()) -
cursor);
result += "<" + match[1].str() + match[2].str() + " />";
cursor = static_cast<std::size_t>(match.position() + match.length());
}
result.append(html, cursor, std::string::npos);
return result;
}
std::string escapeUnsupportedNamedEntities(const std::string& html) {
static const std::unordered_set<std::string> xmlEntities = {
"amp", "apos", "gt", "lt", "quot"};
const std::regex entity(R"(&([A-Za-z][A-Za-z0-9]+);)");
std::string result;
std::size_t cursor = 0;
for (std::sregex_iterator iterator(html.begin(), html.end(), entity), end;
iterator != end; ++iterator) {
const auto& match = *iterator;
result.append(html, cursor, static_cast<std::size_t>(match.position()) -
cursor);
const std::string name = match[1].str();
if (name == "nbsp") {
result += "&#160;";
} else {
result += xmlEntities.contains(name) ? match.str()
: "&amp;" + name + ";";
}
cursor = static_cast<std::size_t>(match.position() + match.length());
}
result.append(html, cursor, std::string::npos);
return result;
}
void removeUnsafeContent(pugi::xml_node node) {
for (pugi::xml_node child = node.first_child(); child;) {
pugi::xml_node next = child.next_sibling();
const std::string name = lowercase(child.name());
if (name == "script" || name == "iframe" || name == "object" ||
name == "embed") {
node.remove_child(child);
} else {
for (pugi::xml_attribute attribute = child.first_attribute();
attribute;) {
pugi::xml_attribute nextAttribute = attribute.next_attribute();
const std::string attributeName = lowercase(attribute.name());
if (attributeName.rfind("on", 0) == 0) {
child.remove_attribute(attribute);
}
attribute = nextAttribute;
}
removeUnsafeContent(child);
}
child = next;
}
}
std::string serializeChildren(const pugi::xml_node& root) {
std::ostringstream output;
for (const pugi::xml_node& child : root.children()) {
child.print(output, "", pugi::format_raw, pugi::encoding_utf8);
}
return output.str();
}
void processImages(
pugi::xml_node node,
ZipWriter& zip,
std::unordered_map<std::string, EpubAsset>& knownAssets,
std::vector<EpubAsset>& assets) {
for (pugi::xml_node child = node.first_child(); child;) {
pugi::xml_node next = child.next_sibling();
if (lowercase(child.name()) != "img") {
processImages(child, zip, knownAssets, assets);
child = next;
continue;
}
pugi::xml_attribute source = child.attribute("src");
const std::filesystem::path localPath =
source ? localPathFromUri(source.value()) : std::filesystem::path();
if (localPath.empty() || !std::filesystem::is_regular_file(localPath)) {
node.remove_child(child);
child = next;
continue;
}
const std::string key =
std::filesystem::weakly_canonical(localPath).string();
auto assetIterator = knownAssets.find(key);
if (assetIterator == knownAssets.end()) {
const std::string extension = normalizedImageExtension(localPath);
const std::string index = std::to_string(assets.size());
EpubAsset asset{
"image-" + index,
"EPUB/images/image-" + index + extension,
imageMediaType(extension),
};
zip.addFile(asset.archivePath, localPath);
assets.push_back(asset);
assetIterator = knownAssets.emplace(key, std::move(asset)).first;
}
const std::string relativePath =
"../images/" +
std::filesystem::path(assetIterator->second.archivePath)
.filename()
.string();
source.set_value(relativePath.c_str());
child.remove_attribute("srcset");
child = next;
}
}
} // namespace
std::filesystem::path localPathFromUri(const std::string& uri) {
constexpr const char* FILE_SCHEME = "file://";
if (uri.rfind(FILE_SCHEME, 0) == 0) {
return percentDecode(uri.substr(7));
}
if (!uri.empty() && uri.front() == '/') {
return percentDecode(uri);
}
return {};
}
std::string normalizedImageExtension(const std::filesystem::path& path) {
const std::string extension = lowercase(path.extension().string());
static const std::unordered_set<std::string> supported = {
".avif", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"};
return supported.contains(extension) ? extension : ".png";
}
std::string imageMediaType(const std::string& extension) {
if (extension == ".avif") {
return "image/avif";
}
if (extension == ".gif") {
return "image/gif";
}
if (extension == ".jpeg" || extension == ".jpg") {
return "image/jpeg";
}
if (extension == ".svg") {
return "image/svg+xml";
}
if (extension == ".webp") {
return "image/webp";
}
return "image/png";
}
std::string prepareChapterBody(
const EpubArchiveChapter& chapter,
ZipWriter& zip,
std::unordered_map<std::string, EpubAsset>& knownAssets,
std::vector<EpubAsset>& assets) {
std::string body = escapeUnsupportedNamedEntities(
makeVoidElementsXmlCompatible(extractBody(readTextFile(chapter.htmlPath))));
pugi::xml_document document;
const std::string wrapped = "<root>" + body + "</root>";
const pugi::xml_parse_result parsed =
document.load_string(wrapped.c_str(), pugi::parse_default);
if (!parsed) {
throw std::runtime_error(
"Downloaded chapter is not valid XHTML near byte " +
std::to_string(parsed.offset) + ": " + parsed.description());
}
pugi::xml_node root = document.child("root");
removeUnsafeContent(root);
processImages(root, zip, knownAssets, assets);
return serializeChildren(root);
}
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,23 @@
#pragma once
#include "EpubAsset.hpp"
#include "EpubExporter.hpp"
#include "ZipWriter.hpp"
#include <filesystem>
#include <string>
#include <unordered_map>
#include <vector>
namespace margelo::nitro::nitroepub {
std::filesystem::path localPathFromUri(const std::string& uri);
std::string normalizedImageExtension(const std::filesystem::path& path);
std::string imageMediaType(const std::string& extension);
std::string prepareChapterBody(
const EpubArchiveChapter& chapter,
ZipWriter& zip,
std::unordered_map<std::string, EpubAsset>& knownAssets,
std::vector<EpubAsset>& assets);
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,217 @@
#include "EpubDocuments.hpp"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <sstream>
namespace margelo::nitro::nitroepub {
namespace {
std::string xmlEscape(const std::string& value) {
std::string escaped;
escaped.reserve(value.size());
for (const char character : value) {
switch (character) {
case '&':
escaped += "&amp;";
break;
case '<':
escaped += "&lt;";
break;
case '>':
escaped += "&gt;";
break;
case '"':
escaped += "&quot;";
break;
case '\'':
escaped += "&apos;";
break;
default:
escaped += character;
break;
}
}
return escaped;
}
std::string modifiedTimestamp() {
const std::time_t now = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now());
std::tm utc{};
#ifdef _WIN32
gmtime_s(&utc, &now);
#else
gmtime_r(&now, &utc);
#endif
std::ostringstream timestamp;
timestamp << std::put_time(&utc, "%Y-%m-%dT%H:%M:%SZ");
return timestamp.str();
}
} // namespace
std::string containerDocument() {
return R"(<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="EPUB/content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
)";
}
std::string chapterDocument(const EpubArchiveChapter& chapter,
const std::string& body,
bool hasJavaScript) {
const std::string title = xmlEscape(chapter.title);
const std::string script =
hasJavaScript ? R"(
<script type="text/javascript" src="../script.js"></script>)"
: "";
const std::string onLoad =
hasJavaScript ? " onload=\"fnEpub()\"" : "";
return R"(<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head>
<meta charset="UTF-8"/>
<title>)" +
title +
R"(</title>
<link rel="stylesheet" type="text/css" href="../styles.css"/>)" +
script + R"(
</head>
<body data-novel-id=")" +
xmlEscape(chapter.novelId) + R"(" data-chapter-id=")" +
xmlEscape(chapter.chapterId) + R"(")" + onLoad + R"(>
)" + body + R"(
</body>
</html>
)";
}
std::string navigationDocument(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters) {
std::ostringstream items;
for (std::size_t index = 0; index < chapters.size(); ++index) {
items << " <li><a href=\"text/chapter-" << index << ".xhtml\">"
<< xmlEscape(chapters[index].title) << "</a></li>\n";
}
return R"(<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">
<head><title>)" +
xmlEscape(metadata.title) + R"(</title></head>
<body>
<nav epub:type="toc" id="toc">
<h1>)" +
xmlEscape(metadata.title) + R"(</h1>
<ol>
)" + items.str() +
R"( </ol>
</nav>
</body>
</html>
)";
}
std::string ncxDocument(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters) {
std::ostringstream points;
for (std::size_t index = 0; index < chapters.size(); ++index) {
points << " <navPoint id=\"chapter-" << index
<< "\" playOrder=\"" << index + 1 << "\">\n"
<< " <navLabel><text>"
<< xmlEscape(chapters[index].title) << "</text></navLabel>\n"
<< " <content src=\"text/chapter-" << index
<< ".xhtml\"/>\n"
<< " </navPoint>\n";
}
return R"(<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
<head><meta name="dtb:uid" content=")" +
xmlEscape(metadata.bookId) + R"("/></head>
<docTitle><text>)" +
xmlEscape(metadata.title) + R"(</text></docTitle>
<navMap>
)" + points.str() +
R"( </navMap>
</ncx>
)";
}
std::string packageDocument(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters,
const std::vector<EpubAsset>& assets,
const EpubAsset* cover,
bool hasJavaScript) {
std::ostringstream manifest;
manifest << " <item id=\"nav\" href=\"nav.xhtml\" "
"media-type=\"application/xhtml+xml\" properties=\"nav\"/>\n"
<< " <item id=\"ncx\" href=\"toc.ncx\" "
"media-type=\"application/x-dtbncx+xml\"/>\n"
<< " <item id=\"styles\" href=\"styles.css\" "
"media-type=\"text/css\"/>\n";
if (hasJavaScript) {
manifest << " <item id=\"script\" href=\"script.js\" "
"media-type=\"text/javascript\"/>\n";
}
if (cover != nullptr) {
manifest << " <item id=\"cover-image\" href=\""
<< xmlEscape(cover->archivePath.substr(5))
<< "\" media-type=\"" << cover->mediaType
<< "\" properties=\"cover-image\"/>\n";
}
for (const EpubAsset& asset : assets) {
manifest << " <item id=\"" << asset.id << "\" href=\""
<< xmlEscape(asset.archivePath.substr(5))
<< "\" media-type=\"" << asset.mediaType << "\"/>\n";
}
for (std::size_t index = 0; index < chapters.size(); ++index) {
manifest << " <item id=\"chapter-" << index
<< "\" href=\"text/chapter-" << index
<< ".xhtml\" media-type=\"application/xhtml+xml\"";
if (hasJavaScript) {
manifest << " properties=\"scripted\"";
}
manifest << "/>\n";
}
std::ostringstream spine;
for (std::size_t index = 0; index < chapters.size(); ++index) {
spine << " <itemref idref=\"chapter-" << index << "\"/>\n";
}
return R"(<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="book-id" xml:lang=")" +
xmlEscape(metadata.language) + R"(">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:identifier id="book-id">)" +
xmlEscape(metadata.bookId) + R"(</dc:identifier>
<dc:title>)" +
xmlEscape(metadata.title) + R"(</dc:title>
<dc:language>)" +
xmlEscape(metadata.language) + R"(</dc:language>
<dc:creator>)" +
xmlEscape(metadata.author) + R"(</dc:creator>
<dc:description>)" +
xmlEscape(metadata.description) + R"(</dc:description>
<meta property="dcterms:modified">)" +
modifiedTimestamp() + R"(</meta>
</metadata>
<manifest>
)" + manifest.str() +
R"( </manifest>
<spine toc="ncx">
)" + spine.str() +
R"( </spine>
</package>
)";
}
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,28 @@
#pragma once
#include "EpubAsset.hpp"
#include "EpubExporter.hpp"
#include <string>
#include <vector>
namespace margelo::nitro::nitroepub {
std::string containerDocument();
std::string chapterDocument(const EpubArchiveChapter& chapter,
const std::string& body,
bool hasJavaScript);
std::string navigationDocument(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters);
std::string ncxDocument(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters);
std::string packageDocument(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters,
const std::vector<EpubAsset>& assets,
const EpubAsset* cover,
bool hasJavaScript);
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,102 @@
#include "EpubExporter.hpp"
#include "EpubAsset.hpp"
#include "EpubContentProcessor.hpp"
#include "EpubDocuments.hpp"
#include "ZipWriter.hpp"
#include <filesystem>
#include <stdexcept>
#include <unordered_map>
namespace margelo::nitro::nitroepub {
EpubArchiveResult exportEpubArchive(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters,
const std::string& outputPath) {
if (metadata.title.empty()) {
throw std::runtime_error("EPUB title cannot be empty");
}
if (outputPath.empty()) {
throw std::runtime_error("EPUB output path cannot be empty");
}
std::vector<EpubArchiveChapter> availableChapters;
availableChapters.reserve(chapters.size());
for (const EpubArchiveChapter& chapter : chapters) {
if (std::filesystem::is_regular_file(chapter.htmlPath)) {
availableChapters.push_back(chapter);
}
}
if (availableChapters.empty()) {
throw std::runtime_error("EPUB requires at least one downloaded chapter");
}
const std::filesystem::path finalPath(outputPath);
const std::filesystem::path temporaryPath = finalPath.string() + ".tmp";
if (!finalPath.parent_path().empty()) {
std::filesystem::create_directories(finalPath.parent_path());
}
std::error_code ignored;
std::filesystem::remove(temporaryPath, ignored);
try {
ZipWriter zip(temporaryPath);
// EPUB requires this exact file to be the first entry and uncompressed.
zip.addStored("mimetype", "application/epub+zip");
zip.addDeflated("META-INF/container.xml", containerDocument());
zip.addDeflated("EPUB/styles.css", metadata.stylesheet);
const bool hasJavaScript = !metadata.javascript.empty();
if (hasJavaScript) {
zip.addDeflated("EPUB/script.js",
"function fnEpub(){\n" + metadata.javascript + "\n}\n");
}
std::vector<EpubAsset> assets;
std::unordered_map<std::string, EpubAsset> knownAssets;
EpubAsset cover;
EpubAsset* coverPointer = nullptr;
const std::filesystem::path coverPath =
localPathFromUri(metadata.coverPath);
if (!coverPath.empty() && std::filesystem::is_regular_file(coverPath)) {
const std::string extension = normalizedImageExtension(coverPath);
cover = EpubAsset{
"cover-image",
"EPUB/images/cover" + extension,
imageMediaType(extension),
};
zip.addFile(cover.archivePath, coverPath);
coverPointer = &cover;
}
for (std::size_t index = 0; index < availableChapters.size(); ++index) {
const std::string body = prepareChapterBody(
availableChapters[index], zip, knownAssets, assets);
zip.addDeflated(
"EPUB/text/chapter-" + std::to_string(index) + ".xhtml",
chapterDocument(availableChapters[index], body, hasJavaScript));
}
zip.addDeflated(
"EPUB/nav.xhtml",
navigationDocument(metadata, availableChapters));
zip.addDeflated("EPUB/toc.ncx", ncxDocument(metadata, availableChapters));
zip.addDeflated(
"EPUB/content.opf",
packageDocument(metadata, availableChapters, assets, coverPointer,
hasJavaScript));
zip.finish();
std::filesystem::remove(finalPath, ignored);
std::filesystem::rename(temporaryPath, finalPath);
return EpubArchiveResult{outputPath, availableChapters.size()};
} catch (...) {
std::filesystem::remove(temporaryPath, ignored);
throw;
}
}
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,37 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
namespace margelo::nitro::nitroepub {
struct EpubArchiveMetadata {
std::string title;
std::string language;
std::string coverPath;
std::string description;
std::string author;
std::string bookId;
std::string stylesheet;
std::string javascript;
};
struct EpubArchiveChapter {
std::string title;
std::string htmlPath;
std::string novelId;
std::string chapterId;
};
struct EpubArchiveResult {
std::string outputPath;
std::size_t chapterCount;
};
EpubArchiveResult exportEpubArchive(
const EpubArchiveMetadata& metadata,
const std::vector<EpubArchiveChapter>& chapters,
const std::string& outputPath);
} // namespace margelo::nitro::nitroepub
+213
View File
@@ -0,0 +1,213 @@
#include "ZipWriter.hpp"
#include <algorithm>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <zlib.h>
namespace margelo::nitro::nitroepub {
namespace {
constexpr std::uint16_t UTF8_FLAG = 0x0800;
constexpr std::uint16_t STORED = 0;
constexpr std::uint16_t DEFLATED = 8;
constexpr std::uint16_t DOS_DATE_1980_01_01 = 0x0021;
std::uint32_t checked32(std::uint64_t value, const char* description) {
if (value > std::numeric_limits<std::uint32_t>::max()) {
throw std::runtime_error(std::string("EPUB exceeds ZIP32 ") + description);
}
return static_cast<std::uint32_t>(value);
}
std::uint16_t checked16(std::size_t value, const char* description) {
if (value > std::numeric_limits<std::uint16_t>::max()) {
throw std::runtime_error(std::string("EPUB exceeds ZIP16 ") + description);
}
return static_cast<std::uint16_t>(value);
}
std::vector<std::uint8_t> deflateRaw(const std::vector<std::uint8_t>& input) {
z_stream stream{};
if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8,
Z_DEFAULT_STRATEGY) != Z_OK) {
throw std::runtime_error("Failed to initialize EPUB compression");
}
std::vector<std::uint8_t> output(
std::max<std::size_t>(128, compressBound(input.size())));
stream.next_in = const_cast<Bytef*>(
reinterpret_cast<const Bytef*>(input.data()));
stream.avail_in = checked32(input.size(), "entry size");
stream.next_out = reinterpret_cast<Bytef*>(output.data());
stream.avail_out = checked32(output.size(), "compressed entry size");
const int result = deflate(&stream, Z_FINISH);
if (result != Z_STREAM_END) {
deflateEnd(&stream);
throw std::runtime_error("Failed to compress EPUB entry");
}
output.resize(stream.total_out);
deflateEnd(&stream);
return output;
}
} // namespace
ZipWriter::ZipWriter(const std::filesystem::path& outputPath)
: output_(outputPath, std::ios::binary | std::ios::trunc) {
if (!output_) {
throw std::runtime_error("Unable to create EPUB at " + outputPath.string());
}
}
ZipWriter::~ZipWriter() {
if (!finished_) {
output_.close();
}
}
void ZipWriter::addStored(const std::string& archivePath,
const std::string& content) {
add(archivePath,
std::vector<std::uint8_t>(content.begin(), content.end()), false);
}
void ZipWriter::addDeflated(const std::string& archivePath,
const std::string& content) {
add(archivePath,
std::vector<std::uint8_t>(content.begin(), content.end()), true);
}
void ZipWriter::addFile(const std::string& archivePath,
const std::filesystem::path& sourcePath,
bool compress) {
std::ifstream input(sourcePath, std::ios::binary);
if (!input) {
throw std::runtime_error("Unable to read EPUB asset " + sourcePath.string());
}
std::vector<std::uint8_t> content(
(std::istreambuf_iterator<char>(input)),
std::istreambuf_iterator<char>());
add(archivePath, content, compress);
}
void ZipWriter::add(const std::string& archivePath,
const std::vector<std::uint8_t>& content,
bool compress) {
if (finished_) {
throw std::runtime_error("Cannot add an entry to a finished EPUB");
}
if (archivePath.empty() || archivePath.front() == '/' ||
archivePath.find("..") != std::string::npos) {
throw std::runtime_error("Unsafe EPUB archive path: " + archivePath);
}
const auto compressed = compress ? deflateRaw(content) : content;
const auto crc = static_cast<std::uint32_t>(
crc32(0, reinterpret_cast<const Bytef*>(content.data()),
checked32(content.size(), "entry size")));
const auto offset = checked32(static_cast<std::uint64_t>(output_.tellp()),
"local header offset");
const auto pathLength = checked16(archivePath.size(), "entry name length");
const auto method = static_cast<std::uint16_t>(compress ? DEFLATED : STORED);
write32(0x04034b50);
write16(20);
write16(UTF8_FLAG);
write16(method);
write16(0);
write16(DOS_DATE_1980_01_01);
write32(crc);
write32(checked32(compressed.size(), "compressed entry size"));
write32(checked32(content.size(), "entry size"));
write16(pathLength);
write16(0);
output_.write(archivePath.data(), pathLength);
output_.write(reinterpret_cast<const char*>(compressed.data()),
static_cast<std::streamsize>(compressed.size()));
if (!output_) {
throw std::runtime_error("Failed while writing EPUB entry " + archivePath);
}
entries_.push_back(CentralEntry{
archivePath,
method,
crc,
checked32(compressed.size(), "compressed entry size"),
checked32(content.size(), "entry size"),
offset,
});
}
void ZipWriter::finish() {
if (finished_) {
return;
}
const auto directoryOffset =
checked32(static_cast<std::uint64_t>(output_.tellp()),
"central directory offset");
for (const auto& entry : entries_) {
const auto pathLength =
checked16(entry.archivePath.size(), "entry name length");
write32(0x02014b50);
write16(20);
write16(20);
write16(UTF8_FLAG);
write16(entry.method);
write16(0);
write16(DOS_DATE_1980_01_01);
write32(entry.crc);
write32(entry.compressedSize);
write32(entry.uncompressedSize);
write16(pathLength);
write16(0);
write16(0);
write16(0);
write16(0);
write32(0);
write32(entry.localHeaderOffset);
output_.write(entry.archivePath.data(), pathLength);
}
const auto directoryEnd =
checked32(static_cast<std::uint64_t>(output_.tellp()),
"central directory size");
const auto entryCount = checked16(entries_.size(), "entry count");
write32(0x06054b50);
write16(0);
write16(0);
write16(entryCount);
write16(entryCount);
write32(directoryEnd - directoryOffset);
write32(directoryOffset);
write16(0);
output_.flush();
if (!output_) {
throw std::runtime_error("Failed to finalize EPUB archive");
}
output_.close();
finished_ = true;
}
void ZipWriter::write16(std::uint16_t value) {
const char bytes[] = {
static_cast<char>(value & 0xff),
static_cast<char>((value >> 8) & 0xff),
};
output_.write(bytes, sizeof(bytes));
}
void ZipWriter::write32(std::uint32_t value) {
const char bytes[] = {
static_cast<char>(value & 0xff),
static_cast<char>((value >> 8) & 0xff),
static_cast<char>((value >> 16) & 0xff),
static_cast<char>((value >> 24) & 0xff),
};
output_.write(bytes, sizeof(bytes));
}
} // namespace margelo::nitro::nitroepub
@@ -0,0 +1,47 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace margelo::nitro::nitroepub {
class ZipWriter final {
public:
explicit ZipWriter(const std::filesystem::path& outputPath);
~ZipWriter();
ZipWriter(const ZipWriter&) = delete;
ZipWriter& operator=(const ZipWriter&) = delete;
void addStored(const std::string& archivePath, const std::string& content);
void addDeflated(const std::string& archivePath, const std::string& content);
void addFile(const std::string& archivePath,
const std::filesystem::path& sourcePath,
bool compress = false);
void finish();
private:
struct CentralEntry {
std::string archivePath;
std::uint16_t method;
std::uint32_t crc;
std::uint32_t compressedSize;
std::uint32_t uncompressedSize;
std::uint32_t localHeaderOffset;
};
std::ofstream output_;
std::vector<CentralEntry> entries_;
bool finished_ = false;
void add(const std::string& archivePath,
const std::vector<std::uint8_t>& content,
bool compress);
void write16(std::uint16_t value);
void write32(std::uint32_t value);
};
} // namespace margelo::nitro::nitroepub
@@ -1,5 +1,5 @@
#include <string>
#include <pugixml.hpp>
#include "../pugixml.hpp"
#include <iostream>
#include <unordered_map>
#include <vector>
@@ -300,4 +300,4 @@ EpubMetadata parseEpub(const std::string epub_path)
parse_opf_from_folder(epub_path, opf_path, metadata);
return metadata;
}
}
@@ -0,0 +1,93 @@
#include "../export/EpubExporter.hpp"
#include <cassert>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
using namespace margelo::nitro::nitroepub;
namespace {
std::uint16_t read16(const std::vector<std::uint8_t>& bytes,
std::size_t offset) {
return static_cast<std::uint16_t>(bytes[offset]) |
static_cast<std::uint16_t>(bytes[offset + 1] << 8);
}
} // namespace
int main(int argc, char** argv) {
assert(argc == 2);
const std::filesystem::path fixtureDirectory = argv[1];
std::filesystem::remove_all(fixtureDirectory);
std::filesystem::create_directories(fixtureDirectory);
const std::filesystem::path imagePath = fixtureDirectory / "image.png";
{
const std::uint8_t png[] = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
};
std::ofstream image(imagePath, std::ios::binary);
image.write(reinterpret_cast<const char*>(png), sizeof(png));
}
const std::filesystem::path chapterPath =
fixtureDirectory / "chapter.html";
{
std::ofstream chapter(chapterPath);
chapter << "<html><body><h1>One &amp; Two</h1>"
<< "<img src=\"file://" << imagePath.string() << "\">"
<< "<img src=\"file:///missing.png\">"
<< "<script>alert('removed')</script></body></html>";
}
const std::filesystem::path outputPath = fixtureDirectory / "fixture.epub";
const EpubArchiveResult result = exportEpubArchive(
EpubArchiveMetadata{
"Fixture & Book",
"en",
"",
"Description",
"LNReader",
"urn:lnreader:test",
"body { line-height: 1.5; }",
"",
},
{
EpubArchiveChapter{
"Chapter <One>",
chapterPath.string(),
"1",
"2",
},
EpubArchiveChapter{
"Missing chapter",
(fixtureDirectory / "missing.html").string(),
"1",
"3",
},
},
outputPath.string());
assert(result.chapterCount == 1);
assert(std::filesystem::is_regular_file(outputPath));
std::ifstream epub(outputPath, std::ios::binary);
const std::vector<std::uint8_t> bytes(
(std::istreambuf_iterator<char>(epub)),
std::istreambuf_iterator<char>());
assert(bytes.size() > 30);
assert(bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 &&
bytes[3] == 0x04);
assert(read16(bytes, 8) == 0);
const std::uint16_t nameLength = read16(bytes, 26);
const std::string firstName(bytes.begin() + 30,
bytes.begin() + 30 + nameLength);
assert(firstName == "mimetype");
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
{
"$schema": "https://nitro.margelo.com/nitro.schema.json",
"cxxNamespace": [
"nitroepub"
],
"ios": {
"iosModuleName": "NitroEpub"
},
"android": {
"androidNamespace": [
"nitroepub"
],
"androidCxxLibName": "NitroEpub"
},
"autolinking": {
"Epub": {
"all": {
"language": "c++",
"implementationClassName": "HybridEpub"
}
}
},
"ignorePaths": [
"**/node_modules"
]
}
@@ -1,5 +1,5 @@
#
# NitroImportEpub+autolinking.cmake
# NitroEpub+autolinking.cmake
# This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
# https://github.com/mrousavy/nitro
# Copyright © Marc Rousavy @ Margelo
@@ -10,11 +10,11 @@
#
# To use it, add this to your CMakeLists.txt:
# ```cmake
# include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroImportEpub+autolinking.cmake)
# include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroEpub+autolinking.cmake)
# ```
# Define a flag to check if we are building properly
add_definitions(-DBUILDING_NITROIMPORTEPUB_WITH_GENERATED_CMAKE_PROJECT)
add_definitions(-DBUILDING_NITROEPUB_WITH_GENERATED_CMAKE_PROJECT)
# Enable Raw Props parsing in react-native (for Nitro Views)
add_definitions(-DRN_SERIALIZABLE_STATE)
@@ -29,9 +29,9 @@ include_directories(
# Add all .cpp sources that were generated by Nitrogen
target_sources(
# CMake project name (Android C++ library name)
NitroImportEpub PRIVATE
NitroEpub PRIVATE
# Autolinking Setup
../nitrogen/generated/android/NitroImportEpubOnLoad.cpp
../nitrogen/generated/android/NitroEpubOnLoad.cpp
# Shared Nitrogen C++ sources
../nitrogen/generated/shared/c++/HybridEpubSpec.cpp
# Android-specific Nitrogen C++ sources
@@ -41,7 +41,7 @@ target_sources(
# From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake
# Used in node_modules/react-native/ReactAndroid/cmake-utils/ReactNative-application.cmake
target_compile_definitions(
NitroImportEpub PRIVATE
NitroEpub PRIVATE
-DFOLLY_NO_CONFIG=1
-DFOLLY_HAVE_CLOCK_GETTIME=1
-DFOLLY_USE_LIBCPP=1
@@ -61,7 +61,7 @@ find_package(react-native-nitro-modules REQUIRED) # <-- Used to create all Hybri
# Link all libraries together
target_link_libraries(
NitroImportEpub
NitroEpub
fbjni::fbjni # <-- Facebook C++ JNI helpers
ReactAndroid::jsi # <-- RN: JSI
react-native-nitro-modules::NitroModules # <-- NitroModules Core :)
@@ -70,12 +70,12 @@ target_link_libraries(
# Link react-native (different prefab between RN 0.75 and RN 0.76)
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
target_link_libraries(
NitroImportEpub
NitroEpub
ReactAndroid::reactnative # <-- RN: Native Modules umbrella prefab
)
else()
target_link_libraries(
NitroImportEpub
NitroEpub
ReactAndroid::react_nativemodule_core # <-- RN: TurboModules Core
)
endif()
@@ -1,5 +1,5 @@
///
/// NitroImportEpub+autolinking.gradle
/// NitroEpub+autolinking.gradle
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
@@ -10,10 +10,10 @@
///
/// To use it, add this to your build.gradle:
/// ```gradle
/// apply from: '../nitrogen/generated/android/NitroImportEpub+autolinking.gradle'
/// apply from: '../nitrogen/generated/android/NitroEpub+autolinking.gradle'
/// ```
logger.warn("[NitroModules] 🔥 NitroImportEpub is boosted by nitro!")
logger.warn("[NitroModules] 🔥 NitroEpub is boosted by nitro!")
android {
sourceSets {
@@ -0,0 +1,49 @@
///
/// NitroEpubOnLoad.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#ifndef BUILDING_NITROEPUB_WITH_GENERATED_CMAKE_PROJECT
#error NitroEpubOnLoad.cpp is not being built with the autogenerated CMakeLists.txt project. Is a different CMakeLists.txt building this?
#endif
#include "NitroEpubOnLoad.hpp"
#include <jni.h>
#include <fbjni/fbjni.h>
#include <NitroModules/HybridObjectRegistry.hpp>
#include "HybridEpub.hpp"
namespace margelo::nitro::nitroepub {
int initialize(JavaVM* vm) {
return facebook::jni::initialize(vm, []() {
::margelo::nitro::nitroepub::registerAllNatives();
});
}
void registerAllNatives() {
using namespace margelo::nitro;
using namespace margelo::nitro::nitroepub;
// Register native JNI methods
// Register Nitro Hybrid Objects
HybridObjectRegistry::registerHybridObjectConstructor(
"Epub",
[]() -> std::shared_ptr<HybridObject> {
static_assert(std::is_default_constructible_v<HybridEpub>,
"The HybridObject \"HybridEpub\" is not default-constructible! "
"Create a public constructor that takes zero arguments to be able to autolink this HybridObject.");
return std::make_shared<HybridEpub>();
}
);
}
} // namespace margelo::nitro::nitroepub
@@ -1,5 +1,5 @@
///
/// NitroImportEpubOnLoad.hpp
/// NitroEpubOnLoad.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
@@ -9,21 +9,21 @@
#include <functional>
#include <NitroModules/NitroDefines.hpp>
namespace margelo::nitro::nitroimportepub {
namespace margelo::nitro::nitroepub {
[[deprecated("Use registerNatives() instead.")]]
int initialize(JavaVM* vm);
/**
* Register the native (C++) part of NitroImportEpub, and autolinks all Hybrid Objects.
* Register the native (C++) part of NitroEpub, and autolinks all Hybrid Objects.
* Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`),
* inside a `facebook::jni::initialize(vm, ...)` call.
* Example:
* ```cpp (cpp-adapter.cpp)
* JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
* return facebook::jni::initialize(vm, []() {
* // register all NitroImportEpub HybridObjects
* margelo::nitro::nitroimportepub::registerNatives();
* // register all NitroEpub HybridObjects
* margelo::nitro::nitroepub::registerNatives();
* // any other custom registrations go here.
* });
* }
@@ -31,4 +31,4 @@ namespace margelo::nitro::nitroimportepub {
*/
void registerAllNatives();
} // namespace margelo::nitro::nitroimportepub
} // namespace margelo::nitro::nitroepub
@@ -1,32 +1,32 @@
///
/// NitroImportEpubOnLoad.kt
/// NitroEpubOnLoad.kt
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
package com.margelo.nitro.nitroimportepub
package com.margelo.nitro.nitroepub
import android.util.Log
internal class NitroImportEpubOnLoad {
internal class NitroEpubOnLoad {
companion object {
private const val TAG = "NitroImportEpubOnLoad"
private const val TAG = "NitroEpubOnLoad"
private var didLoad = false
/**
* Initializes the native part of "NitroImportEpub".
* Initializes the native part of "NitroEpub".
* This method is idempotent and can be called more than once.
*/
@JvmStatic
fun initializeNative() {
if (didLoad) return
try {
Log.i(TAG, "Loading NitroImportEpub C++ library...")
System.loadLibrary("NitroImportEpub")
Log.i(TAG, "Successfully loaded NitroImportEpub C++ library!")
Log.i(TAG, "Loading NitroEpub C++ library...")
System.loadLibrary("NitroEpub")
Log.i(TAG, "Successfully loaded NitroEpub C++ library!")
didLoad = true
} catch (e: Error) {
Log.e(TAG, "Failed to load NitroImportEpub C++ library! Is it properly installed and linked? " +
Log.e(TAG, "Failed to load NitroEpub C++ library! Is it properly installed and linked? " +
"Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e)
throw e
}
@@ -1,5 +1,5 @@
#
# NitroImportEpub+autolinking.rb
# NitroEpub+autolinking.rb
# This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
# https://github.com/mrousavy/nitro
# Copyright © Marc Rousavy @ Margelo
@@ -14,13 +14,13 @@
# # ...
#
# # Add all files generated by Nitrogen
# load 'nitrogen/generated/ios/NitroImportEpub+autolinking.rb'
# load 'nitrogen/generated/ios/NitroEpub+autolinking.rb'
# add_nitrogen_files(spec)
# end
# ```
def add_nitrogen_files(spec)
Pod::UI.puts "[NitroModules] 🔥 NitroImportEpub is boosted by nitro!"
Pod::UI.puts "[NitroModules] 🔥 NitroEpub is boosted by nitro!"
spec.dependency "NitroModules"
@@ -37,7 +37,7 @@ def add_nitrogen_files(spec)
# Generated specs
"nitrogen/generated/shared/**/*.{h,hpp}",
# Swift to C++ bridging helpers
"nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.hpp"
"nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.hpp"
]
current_private_header_files = Array(spec.attributes_hash['private_header_files'])
@@ -0,0 +1,17 @@
///
/// NitroEpub-Swift-Cxx-Bridge.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "NitroEpub-Swift-Cxx-Bridge.hpp"
// Include C++ implementation defined types
namespace margelo::nitro::nitroepub::bridge::swift {
} // namespace margelo::nitro::nitroepub::bridge::swift
@@ -1,5 +1,5 @@
///
/// NitroImportEpub-Swift-Cxx-Bridge.hpp
/// NitroEpub-Swift-Cxx-Bridge.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
@@ -20,8 +20,8 @@
* Contains specialized versions of C++ templated types so they can be accessed from Swift,
* as well as helper functions to interact with those C++ types from Swift.
*/
namespace margelo::nitro::nitroimportepub::bridge::swift {
namespace margelo::nitro::nitroepub::bridge::swift {
} // namespace margelo::nitro::nitroimportepub::bridge::swift
} // namespace margelo::nitro::nitroepub::bridge::swift
@@ -1,5 +1,5 @@
///
/// NitroImportEpub-Swift-Cxx-Umbrella.hpp
/// NitroEpub-Swift-Cxx-Umbrella.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
@@ -14,7 +14,7 @@
// C++ helpers for Swift
#include "NitroImportEpub-Swift-Cxx-Bridge.hpp"
#include "NitroEpub-Swift-Cxx-Bridge.hpp"
// Common C++ types used in Swift
#include <NitroModules/ArrayBufferHolder.hpp>
@@ -26,13 +26,13 @@
// Include Swift defined types
#if __has_include("NitroImportEpub-Swift.h")
#if __has_include("NitroEpub-Swift.h")
// This header is generated by Xcode/Swift on every app build.
// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "NitroImportEpub".
#include "NitroImportEpub-Swift.h"
// If it cannot be found, make sure the Swift module's name (= podspec name) is actually "NitroEpub".
#include "NitroEpub-Swift.h"
// Same as above, but used when building with frameworks (`use_frameworks`)
#elif __has_include(<NitroImportEpub/NitroImportEpub-Swift.h>)
#include <NitroImportEpub/NitroImportEpub-Swift.h>
#elif __has_include(<NitroEpub/NitroEpub-Swift.h>)
#include <NitroEpub/NitroEpub-Swift.h>
#else
#error NitroImportEpub's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "NitroImportEpub", and try building the app first.
#error NitroEpub's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "NitroEpub", and try building the app first.
#endif
@@ -0,0 +1,35 @@
///
/// NitroEpubAutolinking.mm
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#import <Foundation/Foundation.h>
#import <NitroModules/HybridObjectRegistry.hpp>
#import <type_traits>
#include "HybridEpub.hpp"
@interface NitroEpubAutolinking : NSObject
@end
@implementation NitroEpubAutolinking
+ (void) load {
using namespace margelo::nitro;
using namespace margelo::nitro::nitroepub;
HybridObjectRegistry::registerHybridObjectConstructor(
"Epub",
[]() -> std::shared_ptr<HybridObject> {
static_assert(std::is_default_constructible_v<HybridEpub>,
"The HybridObject \"HybridEpub\" is not default-constructible! "
"Create a public constructor that takes zero arguments to be able to autolink this HybridObject.");
return std::make_shared<HybridEpub>();
}
);
}
@end
@@ -0,0 +1,16 @@
///
/// NitroEpubAutolinking.swift
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
import NitroModules
// TODO: Use empty enums once Swift supports exporting them as namespaces
// See: https://github.com/swiftlang/swift/pull/83616
public final class NitroEpubAutolinking {
public typealias bridge = margelo.nitro.nitroepub.bridge.swift
}
@@ -32,7 +32,7 @@
#include <string>
namespace margelo::nitro::nitroimportepub {
namespace margelo::nitro::nitroepub {
/**
* A struct which can be represented as a JavaScript object (EpubChapter).
@@ -50,21 +50,21 @@ namespace margelo::nitro::nitroimportepub {
friend bool operator==(const EpubChapter& lhs, const EpubChapter& rhs) = default;
};
} // namespace margelo::nitro::nitroimportepub
} // namespace margelo::nitro::nitroepub
namespace margelo::nitro {
// C++ EpubChapter <> JS EpubChapter (object)
template <>
struct JSIConverter<margelo::nitro::nitroimportepub::EpubChapter> final {
static inline margelo::nitro::nitroimportepub::EpubChapter fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
struct JSIConverter<margelo::nitro::nitroepub::EpubChapter> final {
static inline margelo::nitro::nitroepub::EpubChapter fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::nitroimportepub::EpubChapter(
return margelo::nitro::nitroepub::EpubChapter(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "path")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroimportepub::EpubChapter& arg) {
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroepub::EpubChapter& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "name"), JSIConverter<std::string>::toJSI(runtime, arg.name));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "path"), JSIConverter<std::string>::toJSI(runtime, arg.path));
@@ -0,0 +1,95 @@
///
/// EpubExportChapter.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
namespace margelo::nitro::nitroepub {
/**
* A struct which can be represented as a JavaScript object (EpubExportChapter).
*/
struct EpubExportChapter final {
public:
std::string title SWIFT_PRIVATE;
std::string htmlPath SWIFT_PRIVATE;
std::string novelId SWIFT_PRIVATE;
std::string chapterId SWIFT_PRIVATE;
public:
EpubExportChapter() = default;
explicit EpubExportChapter(std::string title, std::string htmlPath, std::string novelId, std::string chapterId): title(title), htmlPath(htmlPath), novelId(novelId), chapterId(chapterId) {}
public:
friend bool operator==(const EpubExportChapter& lhs, const EpubExportChapter& rhs) = default;
};
} // namespace margelo::nitro::nitroepub
namespace margelo::nitro {
// C++ EpubExportChapter <> JS EpubExportChapter (object)
template <>
struct JSIConverter<margelo::nitro::nitroepub::EpubExportChapter> final {
static inline margelo::nitro::nitroepub::EpubExportChapter fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::nitroepub::EpubExportChapter(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "htmlPath"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "novelId"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapterId")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroepub::EpubExportChapter& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "title"), JSIConverter<std::string>::toJSI(runtime, arg.title));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "htmlPath"), JSIConverter<std::string>::toJSI(runtime, arg.htmlPath));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "novelId"), JSIConverter<std::string>::toJSI(runtime, arg.novelId));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapterId"), JSIConverter<std::string>::toJSI(runtime, arg.chapterId));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "htmlPath")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "novelId")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapterId")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,111 @@
///
/// EpubExportMetadata.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
namespace margelo::nitro::nitroepub {
/**
* A struct which can be represented as a JavaScript object (EpubExportMetadata).
*/
struct EpubExportMetadata final {
public:
std::string title SWIFT_PRIVATE;
std::string language SWIFT_PRIVATE;
std::string coverPath SWIFT_PRIVATE;
std::string description SWIFT_PRIVATE;
std::string author SWIFT_PRIVATE;
std::string bookId SWIFT_PRIVATE;
std::string stylesheet SWIFT_PRIVATE;
std::string javascript SWIFT_PRIVATE;
public:
EpubExportMetadata() = default;
explicit EpubExportMetadata(std::string title, std::string language, std::string coverPath, std::string description, std::string author, std::string bookId, std::string stylesheet, std::string javascript): title(title), language(language), coverPath(coverPath), description(description), author(author), bookId(bookId), stylesheet(stylesheet), javascript(javascript) {}
public:
friend bool operator==(const EpubExportMetadata& lhs, const EpubExportMetadata& rhs) = default;
};
} // namespace margelo::nitro::nitroepub
namespace margelo::nitro {
// C++ EpubExportMetadata <> JS EpubExportMetadata (object)
template <>
struct JSIConverter<margelo::nitro::nitroepub::EpubExportMetadata> final {
static inline margelo::nitro::nitroepub::EpubExportMetadata fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::nitroepub::EpubExportMetadata(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "coverPath"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "description"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bookId"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stylesheet"))),
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "javascript")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroepub::EpubExportMetadata& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "title"), JSIConverter<std::string>::toJSI(runtime, arg.title));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "language"), JSIConverter<std::string>::toJSI(runtime, arg.language));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "coverPath"), JSIConverter<std::string>::toJSI(runtime, arg.coverPath));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "description"), JSIConverter<std::string>::toJSI(runtime, arg.description));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "author"), JSIConverter<std::string>::toJSI(runtime, arg.author));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "bookId"), JSIConverter<std::string>::toJSI(runtime, arg.bookId));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "stylesheet"), JSIConverter<std::string>::toJSI(runtime, arg.stylesheet));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "javascript"), JSIConverter<std::string>::toJSI(runtime, arg.javascript));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "coverPath")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "description")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bookId")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stylesheet")))) return false;
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "javascript")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -0,0 +1,87 @@
///
/// EpubExportResult.hpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#pragma once
#if __has_include(<NitroModules/JSIConverter.hpp>)
#include <NitroModules/JSIConverter.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/NitroDefines.hpp>)
#include <NitroModules/NitroDefines.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/JSIHelpers.hpp>)
#include <NitroModules/JSIHelpers.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
#include <NitroModules/PropNameIDCache.hpp>
#else
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
#endif
#include <string>
namespace margelo::nitro::nitroepub {
/**
* A struct which can be represented as a JavaScript object (EpubExportResult).
*/
struct EpubExportResult final {
public:
std::string outputPath SWIFT_PRIVATE;
double chapterCount SWIFT_PRIVATE;
public:
EpubExportResult() = default;
explicit EpubExportResult(std::string outputPath, double chapterCount): outputPath(outputPath), chapterCount(chapterCount) {}
public:
friend bool operator==(const EpubExportResult& lhs, const EpubExportResult& rhs) = default;
};
} // namespace margelo::nitro::nitroepub
namespace margelo::nitro {
// C++ EpubExportResult <> JS EpubExportResult (object)
template <>
struct JSIConverter<margelo::nitro::nitroepub::EpubExportResult> final {
static inline margelo::nitro::nitroepub::EpubExportResult fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::nitroepub::EpubExportResult(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "outputPath"))),
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapterCount")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroepub::EpubExportResult& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "outputPath"), JSIConverter<std::string>::toJSI(runtime, arg.outputPath));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapterCount"), JSIConverter<double>::toJSI(runtime, arg.chapterCount));
return obj;
}
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
if (!value.isObject()) {
return false;
}
jsi::Object obj = value.getObject(runtime);
if (!nitro::isPlainObject(runtime, obj)) {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "outputPath")))) return false;
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapterCount")))) return false;
return true;
}
};
} // namespace margelo::nitro
@@ -29,16 +29,14 @@
#endif
// Forward declaration of `EpubChapter` to properly resolve imports.
namespace margelo::nitro::nitroimportepub { struct EpubChapter; }
namespace margelo::nitro::nitroepub { struct EpubChapter; }
#include <string>
#include <NitroModules/Null.hpp>
#include <variant>
#include <optional>
#include "EpubChapter.hpp"
#include <vector>
namespace margelo::nitro::nitroimportepub {
namespace margelo::nitro::nitroepub {
/**
* A struct which can be represented as a JavaScript object (EpubNovel).
@@ -46,50 +44,50 @@ namespace margelo::nitro::nitroimportepub {
struct EpubNovel final {
public:
std::string name SWIFT_PRIVATE;
std::optional<std::variant<nitro::NullType, std::string>> cover SWIFT_PRIVATE;
std::optional<std::variant<nitro::NullType, std::string>> summary SWIFT_PRIVATE;
std::optional<std::variant<nitro::NullType, std::string>> author SWIFT_PRIVATE;
std::optional<std::variant<nitro::NullType, std::string>> artist SWIFT_PRIVATE;
std::optional<std::string> cover SWIFT_PRIVATE;
std::optional<std::string> summary SWIFT_PRIVATE;
std::optional<std::string> author SWIFT_PRIVATE;
std::optional<std::string> artist SWIFT_PRIVATE;
std::vector<EpubChapter> chapters SWIFT_PRIVATE;
std::vector<std::string> cssPaths SWIFT_PRIVATE;
std::vector<std::string> imagePaths SWIFT_PRIVATE;
public:
EpubNovel() = default;
explicit EpubNovel(std::string name, std::optional<std::variant<nitro::NullType, std::string>> cover, std::optional<std::variant<nitro::NullType, std::string>> summary, std::optional<std::variant<nitro::NullType, std::string>> author, std::optional<std::variant<nitro::NullType, std::string>> artist, std::vector<EpubChapter> chapters, std::vector<std::string> cssPaths, std::vector<std::string> imagePaths): name(name), cover(cover), summary(summary), author(author), artist(artist), chapters(chapters), cssPaths(cssPaths), imagePaths(imagePaths) {}
explicit EpubNovel(std::string name, std::optional<std::string> cover, std::optional<std::string> summary, std::optional<std::string> author, std::optional<std::string> artist, std::vector<EpubChapter> chapters, std::vector<std::string> cssPaths, std::vector<std::string> imagePaths): name(name), cover(cover), summary(summary), author(author), artist(artist), chapters(chapters), cssPaths(cssPaths), imagePaths(imagePaths) {}
public:
friend bool operator==(const EpubNovel& lhs, const EpubNovel& rhs) = default;
};
} // namespace margelo::nitro::nitroimportepub
} // namespace margelo::nitro::nitroepub
namespace margelo::nitro {
// C++ EpubNovel <> JS EpubNovel (object)
template <>
struct JSIConverter<margelo::nitro::nitroimportepub::EpubNovel> final {
static inline margelo::nitro::nitroimportepub::EpubNovel fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
struct JSIConverter<margelo::nitro::nitroepub::EpubNovel> final {
static inline margelo::nitro::nitroepub::EpubNovel fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::nitroimportepub::EpubNovel(
return margelo::nitro::nitroepub::EpubNovel(
JSIConverter<std::string>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name"))),
JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover"))),
JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary"))),
JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author"))),
JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist"))),
JSIConverter<std::vector<margelo::nitro::nitroimportepub::EpubChapter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author"))),
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist"))),
JSIConverter<std::vector<margelo::nitro::nitroepub::EpubChapter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters"))),
JSIConverter<std::vector<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cssPaths"))),
JSIConverter<std::vector<std::string>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imagePaths")))
);
}
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroimportepub::EpubNovel& arg) {
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroepub::EpubNovel& arg) {
jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "name"), JSIConverter<std::string>::toJSI(runtime, arg.name));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "cover"), JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::toJSI(runtime, arg.cover));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "summary"), JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::toJSI(runtime, arg.summary));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "author"), JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::toJSI(runtime, arg.author));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "artist"), JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::toJSI(runtime, arg.artist));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapters"), JSIConverter<std::vector<margelo::nitro::nitroimportepub::EpubChapter>>::toJSI(runtime, arg.chapters));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "cover"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.cover));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "summary"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.summary));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "author"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.author));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "artist"), JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.artist));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapters"), JSIConverter<std::vector<margelo::nitro::nitroepub::EpubChapter>>::toJSI(runtime, arg.chapters));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "cssPaths"), JSIConverter<std::vector<std::string>>::toJSI(runtime, arg.cssPaths));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "imagePaths"), JSIConverter<std::vector<std::string>>::toJSI(runtime, arg.imagePaths));
return obj;
@@ -103,11 +101,11 @@ namespace margelo::nitro {
return false;
}
if (!JSIConverter<std::string>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name")))) return false;
if (!JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover")))) return false;
if (!JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary")))) return false;
if (!JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author")))) return false;
if (!JSIConverter<std::optional<std::variant<nitro::NullType, std::string>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist")))) return false;
if (!JSIConverter<std::vector<margelo::nitro::nitroimportepub::EpubChapter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author")))) return false;
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist")))) return false;
if (!JSIConverter<std::vector<margelo::nitro::nitroepub::EpubChapter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters")))) return false;
if (!JSIConverter<std::vector<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cssPaths")))) return false;
if (!JSIConverter<std::vector<std::string>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imagePaths")))) return false;
return true;
@@ -7,7 +7,7 @@
#include "HybridEpubSpec.hpp"
namespace margelo::nitro::nitroimportepub {
namespace margelo::nitro::nitroepub {
void HybridEpubSpec::loadHybridMethods() {
// load base methods/properties
@@ -15,7 +15,8 @@ namespace margelo::nitro::nitroimportepub {
// load custom methods/properties
registerHybrids(this, [](Prototype& prototype) {
prototype.registerHybridMethod("parseNovelAndChapters", &HybridEpubSpec::parseNovelAndChapters);
prototype.registerHybridMethod("exportEpub", &HybridEpubSpec::exportEpub);
});
}
} // namespace margelo::nitro::nitroimportepub
} // namespace margelo::nitro::nitroepub
@@ -14,12 +14,23 @@
#endif
// Forward declaration of `EpubNovel` to properly resolve imports.
namespace margelo::nitro::nitroimportepub { struct EpubNovel; }
namespace margelo::nitro::nitroepub { struct EpubNovel; }
// Forward declaration of `EpubExportResult` to properly resolve imports.
namespace margelo::nitro::nitroepub { struct EpubExportResult; }
// Forward declaration of `EpubExportMetadata` to properly resolve imports.
namespace margelo::nitro::nitroepub { struct EpubExportMetadata; }
// Forward declaration of `EpubExportChapter` to properly resolve imports.
namespace margelo::nitro::nitroepub { struct EpubExportChapter; }
#include "EpubNovel.hpp"
#include <NitroModules/Promise.hpp>
#include <string>
#include "EpubExportResult.hpp"
#include "EpubExportMetadata.hpp"
#include "EpubExportChapter.hpp"
#include <vector>
namespace margelo::nitro::nitroimportepub {
namespace margelo::nitro::nitroepub {
using namespace margelo::nitro;
@@ -50,7 +61,8 @@ namespace margelo::nitro::nitroimportepub {
public:
// Methods
virtual EpubNovel parseNovelAndChapters(const std::string& epubDirPath) = 0;
virtual std::shared_ptr<Promise<EpubNovel>> parseNovelAndChapters(const std::string& epubDirPath) = 0;
virtual std::shared_ptr<Promise<EpubExportResult>> exportEpub(const EpubExportMetadata& metadata, const std::vector<EpubExportChapter>& chapters, const std::string& outputPath) = 0;
protected:
// Hybrid Setup
@@ -61,4 +73,4 @@ namespace margelo::nitro::nitroimportepub {
static constexpr auto TAG = "Epub";
};
} // namespace margelo::nitro::nitroimportepub
} // namespace margelo::nitro::nitroepub
@@ -1,7 +1,7 @@
{
"name": "nitro-import-epub",
"name": "nitro-epub",
"version": "0.0.1",
"description": "Nitro module for parsing EPUB files",
"description": "Nitro module for importing and exporting EPUB files",
"main": "lib/index",
"module": "lib/index",
"types": "lib/index.d.ts",
+11
View File
@@ -0,0 +1,11 @@
import { NitroModules } from 'react-native-nitro-modules'
import type { Epub } from './specs/Epub.nitro'
export const epub = NitroModules.createHybridObject<Epub>('Epub')
export type { Epub } from './specs/Epub.nitro'
export type { EpubChapter } from './types/EpubChapter'
export type { EpubExportChapter } from './types/EpubExportChapter'
export type { EpubExportMetadata } from './types/EpubExportMetadata'
export type { EpubExportResult } from './types/EpubExportResult'
export type { EpubNovel } from './types/EpubNovel'
@@ -0,0 +1,27 @@
import type { HybridObject } from 'react-native-nitro-modules'
import type { EpubExportChapter } from '../types/EpubExportChapter'
import type { EpubExportMetadata } from '../types/EpubExportMetadata'
import type { EpubExportResult } from '../types/EpubExportResult'
import type { EpubNovel } from '../types/EpubNovel'
/**
* Imports and exports EPUB publications using shared native C++ code.
*
* @see {@linkcode Epub.parseNovelAndChapters}
* @see {@linkcode Epub.exportEpub}
*/
export interface Epub extends HybridObject<{ android: 'c++'; ios: 'c++' }> {
/**
* Parses metadata and chapter paths from an extracted EPUB directory.
*/
parseNovelAndChapters(epubDirPath: string): Promise<EpubNovel>
/**
* Creates an EPUB archive from downloaded chapter files.
*/
exportEpub(
metadata: EpubExportMetadata,
chapters: EpubExportChapter[],
outputPath: string
): Promise<EpubExportResult>
}
@@ -0,0 +1,11 @@
/**
* Identifies a chapter discovered while parsing an EPUB publication.
*
* @see {@linkcode EpubChapter.path}
*/
export interface EpubChapter {
/** Chapter title presented to the reader. */
name: string
/** Absolute path to the extracted chapter document. */
path: string
}
@@ -0,0 +1,15 @@
/**
* A downloaded chapter included in an exported EPUB publication.
*
* @see {@linkcode EpubExportChapter.htmlPath}
*/
export interface EpubExportChapter {
/** Chapter title displayed in the table of contents. */
title: string
/** Absolute path to the downloaded chapter HTML file. */
htmlPath: string
/** LNReader novel identifier exposed to optional chapter JavaScript. */
novelId: string
/** LNReader chapter identifier exposed to optional chapter JavaScript. */
chapterId: string
}
@@ -0,0 +1,23 @@
/**
* Publication metadata and optional reader assets used to create an EPUB.
*
* @see {@linkcode EpubExportMetadata.bookId}
*/
export interface EpubExportMetadata {
/** Publication title. */
title: string
/** BCP 47 language tag written to the EPUB package. */
language: string
/** Local cover image path or file URI, or an empty string for no cover. */
coverPath: string
/** Publication description. */
description: string
/** Publication author. */
author: string
/** Stable unique identifier for the publication. */
bookId: string
/** CSS included as the publication stylesheet. */
stylesheet: string
/** JavaScript executed when each chapter loads, or an empty string. */
javascript: string
}
@@ -0,0 +1,11 @@
/**
* Describes a successfully created EPUB archive.
*
* @see {@linkcode EpubExportResult.outputPath}
*/
export interface EpubExportResult {
/** Absolute path to the completed EPUB archive. */
outputPath: string
/** Number of downloaded chapters written to the archive. */
chapterCount: number
}
+25
View File
@@ -0,0 +1,25 @@
import type { EpubChapter } from './EpubChapter'
/**
* Metadata and local resources discovered in an extracted EPUB publication.
*
* @see {@linkcode EpubChapter}
*/
export interface EpubNovel {
/** Publication title. */
name: string
/** Absolute path to the extracted cover image, when present. */
cover?: string
/** Publication description, when present. */
summary?: string
/** Publication author, when present. */
author?: string
/** Publication illustrator or artist, when present. */
artist?: string
/** Chapters in reading order. */
chapters: EpubChapter[]
/** Absolute paths to extracted stylesheets. */
cssPaths: string[]
/** Absolute paths to extracted images. */
imagePaths: string[]
}
-38
View File
@@ -1,38 +0,0 @@
# react-native-nitro-template
This is a template for Nitro Modules.
## Usage
Clone this repo, and change all `$$*$$` names according to your `nitro.json` file.
## Contributing
Contribute a change to this template to update it for newer React Native versions.
## Structure
- [`android/`](android): All your `android`-specific implementations.
- [`build.gradle`](android/build.gradle): The gradle build file. This contains four important pieces:
1. Standard react-native library boilerplate code
2. Configures Kotlin (`apply plugin: 'org.jetbrains.kotlin.android'`)
3. Adds all Nitrogen files (`apply from: '.../NitroImportEpub+autolinking.gradle'`)
4. Triggers the native C++ build (via CMake/`externalNativeBuild`)
- [`CMakeLists.txt`](android/CMakeLists.txt): The CMake build file to build C++ code. This contains four important pieces:
1. Creates a library called `NitroImportEpub` (same as in `nitro.json`)
2. Adds all Nitrogen files (`include(.../NitroImportEpub+autolinking.cmake)`)
3. Adds all custom C++ files (only `HybridTestObjectCpp.cpp`)
4. Adds a `cpp-adapter.cpp` file, which autolinks all C++ HybridObjects (only `HybridTestObjectCpp`)
- [`src/main/java/com/margelo/nitro/nitroimportepub/`](android/src/main/java/com/margelo/nitro/nitroimportepub/): All Kotlin implementations.
- [`NitroImportEpubPackage.kt`](android/src/main/java/com/margelo/nitro/nitroimportepub/NitroImportEpubPackage.kt): The react-native package. You need this because the react-native CLI only adds libraries if they have a `*Package.kt` file. In here, you can autolink all Kotlin HybridObjects.
- [`cpp/`](cpp): All your cross-platform implementations. (only `HybridTestObjectCpp.cpp`)
- [`ios/`](ios): All your iOS-specific implementations.
- [`nitrogen/`](nitrogen): All files generated by nitrogen. You should commit this folder to git.
- [`src/`](src): The TypeScript codebase. This defines all HybridObjects and loads them at runtime.
- [`specs/`](src/specs): All HybridObject types. Nitrogen will run on all `*.nitro.ts` files.
- [`nitro.json`](nitro.json): The configuration file for nitrogen. This will define all native namespaces, as well as the library name.
- [`NitroImportEpub.podspec`](NitroImportEpub.podspec): The iOS podspec build file to build the iOS code. This contains three important pieces:
1. Specifies the Pod's name. This must be identical to the name specified in `nitro.json`.
2. Adds all of your `.swift` or `.cpp` files (implementations).
3. Adds all Nitrogen files (`add_nitrogen_files(s)`)
- [`package.json`](package.json): The npm package.json file. `react-native-nitro-modules` should be a `peerDependency`.
@@ -1,5 +0,0 @@
NitroImportEpub_kotlinVersion=2.1.21
NitroImportEpub_minSdkVersion=23
NitroImportEpub_targetSdkVersion=36
NitroImportEpub_compileSdkVersion=36
NitroImportEpub_ndkVersion=29.0.14206865
@@ -1,45 +0,0 @@
#include "HybridEpub.hpp"
#include "EpubParser.hpp"
#include <NitroModules/HybridObjectRegistry.hpp>
namespace margelo::nitro::nitroimportepub {
EpubNovel HybridEpub::parseNovelAndChapters(const std::string& epubDirPath) {
EpubMetadata metadata = parseEpub(epubDirPath);
EpubNovel result;
result.name = metadata.name;
result.cover = metadata.cover.empty()
? std::nullopt
: std::optional(std::variant<nitro::NullType, std::string>(metadata.cover));
result.summary = metadata.summary.empty()
? std::nullopt
: std::optional(std::variant<nitro::NullType, std::string>(metadata.summary));
result.author = metadata.author.empty()
? std::nullopt
: std::optional(std::variant<nitro::NullType, std::string>(metadata.author));
result.artist = metadata.artist.empty()
? std::nullopt
: std::optional(std::variant<nitro::NullType, std::string>(metadata.artist));
for (const auto& ch : metadata.chapters) {
result.chapters.emplace_back(ch.name, ch.path);
}
result.cssPaths = metadata.cssPaths;
result.imagePaths = metadata.imagePaths;
return result;
}
// Register the HybridObject at load time.
__attribute__((constructor))
static void initEpub() {
HybridObjectRegistry::registerHybridObjectConstructor(
"Epub",
[]() -> std::shared_ptr<HybridObject> {
return std::make_shared<HybridEpub>();
}
);
}
} // namespace margelo::nitro::nitroimportepub
@@ -1,15 +0,0 @@
#pragma once
#include "HybridEpubSpec.hpp"
namespace margelo::nitro::nitroimportepub {
class HybridEpub : public HybridEpubSpec {
public:
HybridEpub() : HybridObject(TAG) {}
~HybridEpub() override = default;
EpubNovel parseNovelAndChapters(const std::string& epubDirPath) override;
};
} // namespace margelo::nitro::nitroimportepub
-8
View File
@@ -1,8 +0,0 @@
//
// Bridge.h
// NitroImportEpub
//
// Created by Marc Rousavy on 22.07.24.
//
#pragma once
-19
View File
@@ -1,19 +0,0 @@
{
"$schema": "https://nitro.margelo.com/nitro.schema.json",
"cxxNamespace": [
"nitroimportepub"
],
"ios": {
"iosModuleName": "NitroImportEpub"
},
"android": {
"androidNamespace": [
"nitroimportepub"
],
"androidCxxLibName": "NitroImportEpub"
},
"autolinking": {},
"ignorePaths": [
"**/node_modules"
]
}
@@ -1,41 +0,0 @@
///
/// NitroImportEpubOnLoad.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#ifndef BUILDING_NITROIMPORTEPUB_WITH_GENERATED_CMAKE_PROJECT
#error NitroImportEpubOnLoad.cpp is not being built with the autogenerated CMakeLists.txt project. Is a different CMakeLists.txt building this?
#endif
#include "NitroImportEpubOnLoad.hpp"
#include <jni.h>
#include <fbjni/fbjni.h>
#include <NitroModules/HybridObjectRegistry.hpp>
namespace margelo::nitro::nitroimportepub {
int initialize(JavaVM* vm) {
return facebook::jni::initialize(vm, []() {
::margelo::nitro::nitroimportepub::registerAllNatives();
});
}
void registerAllNatives() {
using namespace margelo::nitro;
using namespace margelo::nitro::nitroimportepub;
// Register native JNI methods
// Register Nitro Hybrid Objects
}
} // namespace margelo::nitro::nitroimportepub
@@ -1,17 +0,0 @@
///
/// NitroImportEpub-Swift-Cxx-Bridge.cpp
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
/// https://github.com/mrousavy/nitro
/// Copyright © Marc Rousavy @ Margelo
///
#include "NitroImportEpub-Swift-Cxx-Bridge.hpp"
// Include C++ implementation defined types
namespace margelo::nitro::nitroimportepub::bridge::swift {
} // namespace margelo::nitro::nitroimportepub::bridge::swift
-1
View File
@@ -1 +0,0 @@
export type { Epub, EpubNovel, EpubChapter } from './specs/Epub.nitro';
@@ -1,21 +0,0 @@
import type { HybridObject } from 'react-native-nitro-modules';
export interface EpubChapter {
name: string;
path: string;
}
export interface EpubNovel {
name: string;
cover: string | null;
summary: string | null;
author: string | null;
artist: string | null;
chapters: EpubChapter[];
cssPaths: string[];
imagePaths: string[];
}
export interface Epub extends HybridObject<{ android: 'c++'; ios: 'c++' }> {
parseNovelAndChapters(epubDirPath: string): EpubNovel;
}
+1 -4
View File
@@ -32,7 +32,6 @@
"nix:emulator": "nix develop .#emulator --extra-experimental-features nix-command --extra-experimental-features flakes"
},
"dependencies": {
"@cd-z/react-native-epub-creator": "^3.0.0",
"@expo/dom-webview": "^57.0.1",
"@gorhom/bottom-sheet": "^5.2.14",
"@legendapp/list": "^3.3.3",
@@ -83,7 +82,6 @@
"react-native-drawer-layout": "^4.2.8",
"react-native-edge-to-edge": "^1.8.1",
"react-native-error-boundary": "^3.1.0",
"react-native-file-access": "^4.0.3",
"react-native-gesture-handler": "^2.32.0",
"react-native-mmkv": "^4.3.2",
"react-native-pager-view": "^8.0.2",
@@ -98,7 +96,6 @@
"react-native-url-polyfill": "^3.0.0",
"react-native-webview": "^13.16.1",
"react-native-worklets": "^0.10.0",
"react-native-zip-archive": "^7.1.1",
"sanitize-html": "^2.17.6",
"urlencode": "^2.0.0",
"zustand": "^5.0.14"
@@ -145,7 +142,7 @@
"native-tts-media-control",
"native-volume-button-listener",
"native-zip-archive",
"nitro-import-epub",
"nitro-epub",
"native-background-tasks",
"@react-native-vector-icons/material-design-icons",
"react-native-saf-x",
@@ -1,15 +0,0 @@
diff --git a/package.json b/package.json
index d8ffb500a037a9f1d1d1b430a3e9464b24df4d79..192d78ee32a8aa8e01338536ba7fadcf50eb8372 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,8 @@
"lib/"
],
"dependencies": {
- "react-native-file-access": "^3.1.0",
- "react-native-zip-archive": "^6.0.3",
+ "react-native-file-access": "^4.0.0",
+ "react-native-zip-archive": "^7.0.0",
"react-native-saf-x": "^2.2.1",
"@cd-z/epub-constructor": "^3.0.0"
}
-82
View File
@@ -4,16 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
'@cd-z/react-native-epub-creator': 1f06df347ba23528a5c6d147c2598f09c4b0f48fe3b232f92d4d321c1fd904f8
importers:
.:
dependencies:
'@cd-z/react-native-epub-creator':
specifier: ^3.0.0
version: 3.0.0(patch_hash=1f06df347ba23528a5c6d147c2598f09c4b0f48fe3b232f92d4d321c1fd904f8)(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
'@expo/dom-webview':
specifier: ^57.0.1
version: 57.0.1(expo@57.0.7)(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
@@ -164,9 +158,6 @@ importers:
react-native-error-boundary:
specifier: ^3.1.0
version: 3.1.0(react-native-safe-area-context@5.8.0(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
react-native-file-access:
specifier: ^4.0.3
version: 4.0.3(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
react-native-gesture-handler:
specifier: ^2.32.0
version: 2.32.0(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
@@ -209,9 +200,6 @@ importers:
react-native-worklets:
specifier: ^0.10.0
version: 0.10.2(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
react-native-zip-archive:
specifier: ^7.1.1
version: 7.1.1(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
sanitize-html:
specifier: ^2.17.6
version: 2.17.6
@@ -1010,15 +998,6 @@ packages:
peerDependencies:
react: '>=16.3.0'
'@cd-z/epub-constructor@3.0.3':
resolution: {integrity: sha512-0Q4DBZ+H6kOihAfREu1dcfwkuwIoOtpXK24im24STP89aIb1HoAhGd3MKLKsZK3tIPVsdfjI2esPnYpmhVXxrw==}
'@cd-z/react-native-epub-creator@3.0.0':
resolution: {integrity: sha512-vpI3viNxkSJWuSCuotFKfe1Ulg7Z+VPBTSt4GkGSGwHwpZxkNnh/UOZqyHBTtn3yvjaq2PK3GABDZlxlDhiQvA==}
peerDependencies:
react: '*'
react-native: '*'
'@drizzle-team/brocli@0.11.0':
resolution: {integrity: sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg==}
@@ -5005,22 +4984,6 @@ packages:
react-native: '>=0.74.0'
react-native-safe-area-context: '>=5.0.0'
react-native-file-access@3.2.0:
resolution: {integrity: sha512-3G0Ma3FvV99Ne7lbwHyZKqLMZVrpIr6fAlIYZQxkRDSZZJ1UtJlGi0VqbJXnJFB26a7C7PEPyVBoctmKr7DzaA==}
peerDependencies:
react: '*'
react-native: '*'
react-native-file-access@4.0.3:
resolution: {integrity: sha512-7xiUcjMhicMM+WmWbO2U2iutXcoYxi3IxScB08kHle6XfBomAV9e2MtszCR7aoSfYNSblpKKqSefra6zFKXxTw==}
peerDependencies:
react: '*'
react-native: '*'
react-native-macos: '*'
peerDependenciesMeta:
react-native-macos:
optional: true
react-native-gesture-handler@2.32.0:
resolution: {integrity: sha512-uYIMOKlKENORq2SABE+jIjbPU+h5I/sQKcq2v16zRq848nwEp1fWRVwML4QWqijc8UcXJC25o54S8GQd4Mf2OA==}
peerDependencies:
@@ -5128,18 +5091,6 @@ packages:
react: '*'
react-native: 0.83 - 0.86
react-native-zip-archive@6.1.2:
resolution: {integrity: sha512-LcJomSY/6O3KHy/LF6Gb7F/yRJiZJ0lTlPQPbfeOHBQzfvqNJFJZ8x6HrdeYeokFf/UGB5bY7jfh4es6Y/PhBA==}
peerDependencies:
react: '>=16.8.6'
react-native: '>=0.60.0'
react-native-zip-archive@7.1.1:
resolution: {integrity: sha512-rlRn8iZCwURU6R1MlrukQT0W84d62UijBmhL8PF32so5J06auDpry+Tmb2dsBEpLKhXCtZgG60uBXukV3hqY3A==}
peerDependencies:
react: '>=16.8.6'
react-native: '>=0.60.0'
react-native@0.86.0:
resolution: {integrity: sha512-17ALh/dd6AO4pgOVmOO5Axll5PbErEo3XFyLokyzW6usyi+OShIEPwUW26wLPlhVifgSOIfECCH0WN+0IqtJ1w==}
engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0}
@@ -6875,19 +6826,6 @@ snapshots:
hoist-non-react-statics: 3.3.2
react: 19.2.3
'@cd-z/epub-constructor@3.0.3':
dependencies:
sanitize-html: 2.17.6
'@cd-z/react-native-epub-creator@3.0.0(patch_hash=1f06df347ba23528a5c6d147c2598f09c4b0f48fe3b232f92d4d321c1fd904f8)(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)':
dependencies:
'@cd-z/epub-constructor': 3.0.3
react: 19.2.3
react-native: 0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0)
react-native-file-access: 3.2.0(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
react-native-saf-x: 2.2.3(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
react-native-zip-archive: 6.1.2(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
'@drizzle-team/brocli@0.11.0': {}
'@egjs/hammerjs@2.0.17':
@@ -11649,16 +11587,6 @@ snapshots:
react-native: 0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0)
react-native-safe-area-context: 5.8.0(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3)
react-native-file-access@3.2.0(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3):
dependencies:
react: 19.2.3
react-native: 0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0)
react-native-file-access@4.0.3(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3):
dependencies:
react: 19.2.3
react-native: 0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0)
react-native-gesture-handler@2.32.0(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3):
dependencies:
'@egjs/hammerjs': 2.0.17
@@ -11779,16 +11707,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
react-native-zip-archive@6.1.2(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3):
dependencies:
react: 19.2.3
react-native: 0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0)
react-native-zip-archive@7.1.1(react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0))(react@19.2.3):
dependencies:
react: 19.2.3
react-native: 0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0)
react-native@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(@react-native/jest-preset@0.86.0(@babel/core@7.29.7(supports-color@9.4.0))(react@19.2.3))(@react-native/metro-config@0.86.0(@babel/core@7.29.7(supports-color@9.4.0)))(@types/react@19.2.17)(react@19.2.3)(supports-color@9.4.0):
dependencies:
'@react-native/assets-registry': 0.86.0
-3
View File
@@ -29,6 +29,3 @@ onlyBuiltDependencies:
- better-sqlite3
- esbuild
- protobufjs
patchedDependencies:
'@cd-z/react-native-epub-creator': patches/@cd-z__react-native-epub-creator.patch
+11
View File
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"build-nitro-modules": {
"source": "margelo/react-native-skills",
"sourceType": "github",
"skillPath": "skills/build-nitro-modules/SKILL.md",
"computedHash": "9fce6b96add81fc0baf4594edd0c70c811c25ab2109d25b16f148c46c6d754b4"
}
}
}
@@ -12,12 +12,19 @@ import { showToast } from '@utils/showToast';
interface ExportEpubModalProps {
isVisible: boolean;
onSubmit?: (uri: string, startChapter?: number, endChapter?: number) => void;
defaultFileName: string;
onSubmit?: (
uri: string,
fileName: string,
startChapter?: number,
endChapter?: number,
) => void;
hideModal: () => void;
}
const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
isVisible,
defaultFileName,
onSubmit: onSubmitProp,
hideModal,
}) => {
@@ -31,6 +38,7 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
} = useChapterReaderSettings();
const [uri, setUri] = useState(epubLocation);
const [fileName, setFileName] = useState(defaultFileName);
const useAppTheme = useBoolean(epubUseAppTheme);
const useCustomCSS = useBoolean(epubUseCustomCSS);
const useCustomJS = useBoolean(epubUseCustomJS);
@@ -41,6 +49,7 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
const onDismiss = () => {
hideModal();
setUri(epubLocation);
setFileName(defaultFileName);
exportAll.setTrue();
setStartChapter('');
setEndChapter('');
@@ -77,7 +86,7 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
const start = exportAll.value ? undefined : parseInt(startChapter, 10);
const end = exportAll.value ? undefined : parseInt(endChapter, 10);
onSubmitProp?.(uri, start, end);
onSubmitProp?.(uri, fileName, start, end);
hideModal();
};
@@ -114,6 +123,18 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
/>
}
/>
<TextInput
label={getString('novelScreen.exportEpubModal.fileName')}
value={fileName}
onChangeText={setFileName}
onSubmitEditing={onSubmit}
mode="outlined"
theme={{ colors: { ...theme } }}
underlineColor={theme.outline}
dense
right={<TextInput.Affix text=".epub" />}
style={styles.fileNameInput}
/>
</Dialog.Content>
<Dialog.List>
<SwitchItem
@@ -195,6 +216,9 @@ const styles = StyleSheet.create({
infoItem: {
paddingHorizontal: 0,
},
fileNameInput: {
marginTop: 12,
},
rangeInputs: {
flexDirection: 'row',
@@ -1,9 +1,10 @@
import React, { useMemo } from 'react';
import { Portal } from 'react-native-paper';
import { StatusBar, StyleProp, ViewStyle } from 'react-native';
import { copyFile, openDocumentTree } from 'react-native-saf-x';
import EpubBuilder from '@cd-z/react-native-epub-creator';
import NativeFile from '@modules/native-file'
import { epub, type EpubExportChapter } from '@modules/nitro-epub';
import NativeFile from '@modules/native-file';
import { NovelInfo } from '@database/types';
import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
@@ -26,6 +27,16 @@ interface ExportNovelAsEpubButtonProps {
}) => React.JSX.Element;
}
const sanitizeEpubFileName = (fileName: string) => {
const withoutExtension = fileName.trim().replace(/\.epub$/i, '');
return (
withoutExtension
.replace(/[\\/:*?"<>|\u0000-\u001f]/g, '')
.replace(/[. ]+$/g, '')
.trim() || 'novel'
);
};
const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
novel,
iconComponent: IconComponent,
@@ -55,7 +66,7 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
padding-top: ${StatusBar.currentHeight ?? 0}px;
word-wrap: break-word;
}
body {
@@ -67,7 +78,7 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
text-align: ${readerSettings.textAlign};
line-height: ${readerSettings.lineHeight};
font-family: "${readerSettings.fontFamily}";
background-color: "${readerSettings.theme}";
background-color: ${readerSettings.theme};
}
hr {
margin-top: 20px;
@@ -99,12 +110,12 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
}
return `
let novelName = "${novel.name}";
let chapterName = "";
let sourceId = ${novel.pluginId};
let chapterId = "";
let novelId = ${novel.id};
let html = document.querySelector("chapter").innerHTML;
let novelName = ${JSON.stringify(novel.name)};
let chapterName = document.title;
let sourceId = ${JSON.stringify(novel.pluginId)};
let chapterId = document.body.dataset.chapterId;
let novelId = ${JSON.stringify(novel.id)};
let html = document.body.innerHTML;
${readerSettings.customJS}
`;
@@ -112,6 +123,7 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
const exportNovelAsEpub = async (
destinationUri: string,
fileName: string,
startChapter?: number,
endChapter?: number,
) => {
@@ -120,7 +132,7 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
return;
}
let epub: EpubBuilder | undefined;
let tempEpubPath: string | undefined;
try {
const chapters = await getNovelDownloadedChapters(
@@ -134,82 +146,53 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
return;
}
epub = new EpubBuilder(
{
title: novel.name,
fileName: novel.name.replace(/[\\/:*?"<>|\s]/g, '') || 'novel',
language: 'en',
cover: novel.cover,
description: novel.summary,
author: novel.author,
bookId: novel.pluginId.toString(),
stylesheet: epubStylesheet || undefined,
js: epubUseCustomJS ? epubJavaScript : undefined,
},
destinationUri,
let resolvedDestinationUri = destinationUri;
if (!resolvedDestinationUri) {
const selectedFolder = await openDocumentTree(true);
if (!selectedFolder) {
return;
}
resolvedDestinationUri = selectedFolder.uri;
}
const epubFileName = `${sanitizeEpubFileName(fileName)}.epub`;
tempEpubPath = `${NativeFile.ExternalCachesDirectoryPath}/epub-export-${
novel.id
}-${Date.now()}.epub`;
const epubChapters: EpubExportChapter[] = chapters.map(
(chapter, index) => ({
title:
chapter.name?.trim() ||
`Chapter ${chapter.chapterNumber || index + 1}`,
htmlPath: `${NOVEL_STORAGE}/${novel.pluginId}/${novel.id}/${chapter.id}/index.html`,
novelId: novel.id.toString(),
chapterId: chapter.id.toString(),
}),
);
await epub.prepare();
const result = await epub.exportEpub(
{
title: novel.name,
language: 'en',
coverPath: novel.cover || '',
description: novel.summary || '',
author: novel.author || '',
bookId: `urn:lnreader:${novel.pluginId}:${novel.id}`,
stylesheet: epubStylesheet,
javascript: epubUseCustomJS ? epubJavaScript : '',
},
epubChapters,
tempEpubPath,
);
let addedChapters = 0;
for (let i = 0; i < chapters.length; i++) {
const chapter = chapters[i];
const chapterFilePath = `${NOVEL_STORAGE}/${novel.pluginId}/${novel.id}/${chapter.id}/index.html`;
if (await NativeFile.exists(chapterFilePath)) {
let chapterContent = await NativeFile.readFile(chapterFilePath);
const chapterDir = `${NOVEL_STORAGE}/${novel.pluginId}/${novel.id}/${chapter.id}`;
const escapedDir = chapterDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const imagePathRegex = new RegExp(
`file://(${escapedDir}/[^"'\\s]+)`,
'g',
);
for (const match of chapterContent.matchAll(imagePathRegex)) {
const imagePath = match[1];
if (imagePath && !(await NativeFile.exists(imagePath))) {
const escapedPath = imagePath.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&',
);
const figureRegex = new RegExp(
`<figure[^>]*>.*?${escapedPath}.*?</figure>`,
'gs',
);
chapterContent = chapterContent.replace(figureRegex, '');
const imgRegex = new RegExp(
`<img[^>]*${escapedPath}[^>]*\\/?>`,
'g',
);
chapterContent = chapterContent.replace(imgRegex, '');
}
}
await epub.addChapter({
title:
chapter.name?.trim() || `Chapter ${chapter.chapterNumber || i}`,
fileName: `Chapter${i}`,
htmlBody: `<chapter data-novel-id='${novel.pluginId}' data-chapter-id='${chapter.id}'>${chapterContent}</chapter>`,
});
addedChapters++;
}
}
if (addedChapters === 0) {
showToast(getString('novelScreen.epub.noDownloadedChapters'));
await epub.discardChanges();
return;
}
await epub.save();
await copyFile(
`file://${result.outputPath}`,
`${resolvedDestinationUri}/${epubFileName}`,
{ replaceIfDestinationExists: true },
);
showToast(
getString('novelScreen.epub.exportSuccess', {
chapters: addedChapters.toString(),
chapters: result.chapterCount.toString(),
}),
);
} catch (error: any) {
@@ -218,7 +201,14 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
error: error.message || error,
}),
);
await epub?.discardChanges();
} finally {
try {
if (tempEpubPath && (await NativeFile.exists(tempEpubPath))) {
await NativeFile.unlink(tempEpubPath);
}
} catch {
// Export cleanup must not replace the original result or error.
}
}
};
@@ -228,6 +218,7 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
<Portal>
<ExportEpubModal
isVisible={isModalVisible}
defaultFileName={novel?.name || 'novel'}
hideModal={hideModal}
onSubmit={exportNovelAsEpub}
/>
+2 -4
View File
@@ -15,8 +15,7 @@ import type {
} from '@services/backgroundTasks/contracts';
import NativeFile from '@modules/native-file';
import NativeZipArchive from '@modules/native-zip-archive';
import { NitroModules } from 'react-native-nitro-modules';
import type { Epub } from '@modules/nitro-import-epub';
import { epub } from '@modules/nitro-epub';
import { showToast } from '@utils/showToast';
const decodePath = (path: string) => {
@@ -137,8 +136,7 @@ export const importEpub = async (
await NativeFile.copyFile(uri, epubFilePath);
await NativeZipArchive.unzip(epubFilePath, epubDirPath);
const epub = NitroModules.createHybridObject<Epub>('Epub');
const novel = epub.parseNovelAndChapters(epubDirPath);
const novel = await epub.parseNovelAndChapters(epubDirPath);
if (!novel.name) {
novel.name = filename.replace('.epub', '') || 'Untitled';
}
+1
View File
@@ -425,6 +425,7 @@
"downloadedChaptersOnly": "Only downloaded chapters will be included in the EPUB file",
"endChapter": "End Chapter",
"exportAll": "Export All Chapters",
"fileName": "File name",
"includeCustomCSS": "Include Custom CSS",
"includeCustomJS": "Include Custom JS",
"invalidRange": "Please enter valid chapter numbers",
+1
View File
@@ -359,6 +359,7 @@ export interface StringMap {
'novelScreen.exportEpubModal.downloadedChaptersOnly': 'string';
'novelScreen.exportEpubModal.endChapter': 'string';
'novelScreen.exportEpubModal.exportAll': 'string';
'novelScreen.exportEpubModal.fileName': 'string';
'novelScreen.exportEpubModal.includeCustomCSS': 'string';
'novelScreen.exportEpubModal.includeCustomJS': 'string';
'novelScreen.exportEpubModal.invalidRange': 'string';