feat: Add Nitro EPUB Exporter
This commit is contained in:
@@ -111,3 +111,4 @@ src/generated/**/*
|
||||
android/
|
||||
!modules/**/android/
|
||||
ios/
|
||||
!modules/**/ios/
|
||||
|
||||
+4
-2
@@ -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
|
||||
@@ -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.
|
||||
+10
-4
@@ -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
|
||||
)
|
||||
+4
-5
@@ -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
|
||||
+2
-2
@@ -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();
|
||||
});
|
||||
}
|
||||
+3
-3
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 += " ";
|
||||
} else {
|
||||
result += xmlEntities.contains(name) ? match.str()
|
||||
: "&" + 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 += "&";
|
||||
break;
|
||||
case '<':
|
||||
escaped += "<";
|
||||
break;
|
||||
case '>':
|
||||
escaped += ">";
|
||||
break;
|
||||
case '"':
|
||||
escaped += """;
|
||||
break;
|
||||
case '\'':
|
||||
escaped += "'";
|
||||
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
|
||||
@@ -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
|
||||
+2
-2
@@ -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 & 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;
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
+9
-9
@@ -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()
|
||||
+3
-3
@@ -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
|
||||
+6
-6
@@ -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
|
||||
+9
-9
@@ -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
|
||||
}
|
||||
+4
-4
@@ -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
|
||||
+3
-3
@@ -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
|
||||
+8
-8
@@ -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
|
||||
|
||||
|
||||
}
|
||||
+6
-6
@@ -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
|
||||
+27
-29
@@ -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;
|
||||
+3
-2
@@ -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
|
||||
+16
-4
@@ -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",
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
//
|
||||
// Bridge.h
|
||||
// NitroImportEpub
|
||||
//
|
||||
// Created by Marc Rousavy on 22.07.24.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
@@ -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 +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
@@ -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"
|
||||
}
|
||||
Generated
-82
@@ -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
|
||||
|
||||
@@ -29,6 +29,3 @@ onlyBuiltDependencies:
|
||||
- better-sqlite3
|
||||
- esbuild
|
||||
- protobufjs
|
||||
|
||||
patchedDependencies:
|
||||
'@cd-z/react-native-epub-creator': patches/@cd-z__react-native-epub-creator.patch
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user