From 4cb1890b62eb57cc75ca0628a11be312c930ddc4 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Fri, 24 Jul 2026 12:30:43 +0530 Subject: [PATCH] feat: Add Nitro EPUB Exporter --- .gitignore | 1 + .../.gitignore | 0 .../.watchmanconfig | 0 .../NitroEpub.podspec} | 6 +- modules/nitro-epub/README.md | 29 ++ .../android/CMakeLists.txt | 14 +- .../android/build.gradle | 9 +- .../android/fix-prefab.gradle | 0 modules/nitro-epub/android/gradle.properties | 5 + .../android/src/main/AndroidManifest.xml | 0 .../android/src/main/cpp/cpp-adapter.cpp | 4 +- .../nitro/nitroepub/NitroEpubPackage.kt} | 6 +- .../babel.config.js | 0 modules/nitro-epub/cpp/HybridEpub.cpp | 70 +++++ modules/nitro-epub/cpp/HybridEpub.hpp | 20 ++ modules/nitro-epub/cpp/export/EpubAsset.hpp | 13 + .../cpp/export/EpubContentProcessor.cpp | 261 ++++++++++++++++++ .../cpp/export/EpubContentProcessor.hpp | 23 ++ .../nitro-epub/cpp/export/EpubDocuments.cpp | 217 +++++++++++++++ .../nitro-epub/cpp/export/EpubDocuments.hpp | 28 ++ .../nitro-epub/cpp/export/EpubExporter.cpp | 102 +++++++ .../nitro-epub/cpp/export/EpubExporter.hpp | 37 +++ modules/nitro-epub/cpp/export/ZipWriter.cpp | 213 ++++++++++++++ modules/nitro-epub/cpp/export/ZipWriter.hpp | 47 ++++ .../cpp/import}/EpubParser.cpp | 4 +- .../cpp/import}/EpubParser.hpp | 0 .../cpp/pugiconfig.hpp | 0 .../cpp/pugixml.cpp | 0 .../cpp/pugixml.hpp | 0 .../nitro-epub/cpp/tests/EpubExporterTest.cpp | 93 +++++++ modules/nitro-epub/nitro.json | 26 ++ .../nitrogen/generated/.gitattributes | 0 .../android/NitroEpub+autolinking.cmake} | 18 +- .../android/NitroEpub+autolinking.gradle} | 6 +- .../generated/android/NitroEpubOnLoad.cpp | 49 ++++ .../generated/android/NitroEpubOnLoad.hpp} | 12 +- .../nitro/nitroepub/NitroEpubOnLoad.kt} | 18 +- .../generated/ios/NitroEpub+autolinking.rb} | 8 +- .../ios/NitroEpub-Swift-Cxx-Bridge.cpp | 17 ++ .../ios/NitroEpub-Swift-Cxx-Bridge.hpp} | 6 +- .../ios/NitroEpub-Swift-Cxx-Umbrella.hpp} | 16 +- .../generated/ios/NitroEpubAutolinking.mm | 35 +++ .../generated/ios/NitroEpubAutolinking.swift | 16 ++ .../generated/shared/c++/EpubChapter.hpp | 12 +- .../shared/c++/EpubExportChapter.hpp | 95 +++++++ .../shared/c++/EpubExportMetadata.hpp | 111 ++++++++ .../generated/shared/c++/EpubExportResult.hpp | 87 ++++++ .../generated/shared/c++/EpubNovel.hpp | 56 ++-- .../generated/shared/c++/HybridEpubSpec.cpp | 5 +- .../generated/shared/c++/HybridEpubSpec.hpp | 20 +- .../package.json | 4 +- .../react-native.config.js | 0 modules/nitro-epub/src/index.ts | 11 + modules/nitro-epub/src/specs/Epub.nitro.ts | 27 ++ modules/nitro-epub/src/types/EpubChapter.ts | 11 + .../nitro-epub/src/types/EpubExportChapter.ts | 15 + .../src/types/EpubExportMetadata.ts | 23 ++ .../nitro-epub/src/types/EpubExportResult.ts | 11 + modules/nitro-epub/src/types/EpubNovel.ts | 25 ++ .../tsconfig.json | 0 modules/nitro-import-epub/README.md | 38 --- .../android/gradle.properties | 5 - modules/nitro-import-epub/cpp/HybridEpub.cpp | 45 --- modules/nitro-import-epub/cpp/HybridEpub.hpp | 15 - modules/nitro-import-epub/ios/Bridge.h | 8 - modules/nitro-import-epub/nitro.json | 19 -- .../android/NitroImportEpubOnLoad.cpp | 41 --- .../ios/NitroImportEpub-Swift-Cxx-Bridge.cpp | 17 -- modules/nitro-import-epub/src/index.ts | 1 - .../nitro-import-epub/src/specs/Epub.nitro.ts | 21 -- package.json | 5 +- .../@cd-z__react-native-epub-creator.patch | 15 - pnpm-lock.yaml | 82 ------ pnpm-workspace.yaml | 3 - skills-lock.json | 11 + .../novel/components/ExportEpubModal.tsx | 28 +- .../components/ExportNovelAsEpubButton.tsx | 157 +++++------ src/services/epub/import.ts | 6 +- strings/languages/en/strings.json | 1 + strings/types/index.ts | 1 + 80 files changed, 1955 insertions(+), 506 deletions(-) rename modules/{nitro-import-epub => nitro-epub}/.gitignore (100%) rename modules/{nitro-import-epub => nitro-epub}/.watchmanconfig (100%) rename modules/{nitro-import-epub/NitroImportEpub.podspec => nitro-epub/NitroEpub.podspec} (84%) create mode 100644 modules/nitro-epub/README.md rename modules/{nitro-import-epub => nitro-epub}/android/CMakeLists.txt (60%) rename modules/{nitro-import-epub => nitro-epub}/android/build.gradle (93%) rename modules/{nitro-import-epub => nitro-epub}/android/fix-prefab.gradle (100%) create mode 100644 modules/nitro-epub/android/gradle.properties rename modules/{nitro-import-epub => nitro-epub}/android/src/main/AndroidManifest.xml (100%) rename modules/{nitro-import-epub => nitro-epub}/android/src/main/cpp/cpp-adapter.cpp (61%) rename modules/{nitro-import-epub/android/src/main/java/com/margelo/nitro/nitroimportepub/NitroImportEpubPackage.kt => nitro-epub/android/src/main/java/com/margelo/nitro/nitroepub/NitroEpubPackage.kt} (76%) rename modules/{nitro-import-epub => nitro-epub}/babel.config.js (100%) create mode 100644 modules/nitro-epub/cpp/HybridEpub.cpp create mode 100644 modules/nitro-epub/cpp/HybridEpub.hpp create mode 100644 modules/nitro-epub/cpp/export/EpubAsset.hpp create mode 100644 modules/nitro-epub/cpp/export/EpubContentProcessor.cpp create mode 100644 modules/nitro-epub/cpp/export/EpubContentProcessor.hpp create mode 100644 modules/nitro-epub/cpp/export/EpubDocuments.cpp create mode 100644 modules/nitro-epub/cpp/export/EpubDocuments.hpp create mode 100644 modules/nitro-epub/cpp/export/EpubExporter.cpp create mode 100644 modules/nitro-epub/cpp/export/EpubExporter.hpp create mode 100644 modules/nitro-epub/cpp/export/ZipWriter.cpp create mode 100644 modules/nitro-epub/cpp/export/ZipWriter.hpp rename modules/{nitro-import-epub/cpp => nitro-epub/cpp/import}/EpubParser.cpp (99%) rename modules/{nitro-import-epub/cpp => nitro-epub/cpp/import}/EpubParser.hpp (100%) rename modules/{nitro-import-epub => nitro-epub}/cpp/pugiconfig.hpp (100%) rename modules/{nitro-import-epub => nitro-epub}/cpp/pugixml.cpp (100%) rename modules/{nitro-import-epub => nitro-epub}/cpp/pugixml.hpp (100%) create mode 100644 modules/nitro-epub/cpp/tests/EpubExporterTest.cpp create mode 100644 modules/nitro-epub/nitro.json rename modules/{nitro-import-epub => nitro-epub}/nitrogen/generated/.gitattributes (100%) rename modules/{nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.cmake => nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.cmake} (88%) rename modules/{nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.gradle => nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.gradle} (72%) create mode 100644 modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.cpp rename modules/{nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.hpp => nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.hpp} (69%) rename modules/{nitro-import-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimportepub/NitroImportEpubOnLoad.kt => nitro-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroepub/NitroEpubOnLoad.kt} (53%) rename modules/{nitro-import-epub/nitrogen/generated/ios/NitroImportEpub+autolinking.rb => nitro-epub/nitrogen/generated/ios/NitroEpub+autolinking.rb} (88%) create mode 100644 modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.cpp rename modules/{nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.hpp => nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.hpp} (74%) rename modules/{nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Umbrella.hpp => nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Umbrella.hpp} (60%) create mode 100644 modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.mm create mode 100644 modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.swift rename modules/{nitro-import-epub => nitro-epub}/nitrogen/generated/shared/c++/EpubChapter.hpp (87%) create mode 100644 modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportChapter.hpp create mode 100644 modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportMetadata.hpp create mode 100644 modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportResult.hpp rename modules/{nitro-import-epub => nitro-epub}/nitrogen/generated/shared/c++/EpubNovel.hpp (50%) rename modules/{nitro-import-epub => nitro-epub}/nitrogen/generated/shared/c++/HybridEpubSpec.cpp (76%) rename modules/{nitro-import-epub => nitro-epub}/nitrogen/generated/shared/c++/HybridEpubSpec.hpp (57%) rename modules/{nitro-import-epub => nitro-epub}/package.json (96%) rename modules/{nitro-import-epub => nitro-epub}/react-native.config.js (100%) create mode 100644 modules/nitro-epub/src/index.ts create mode 100644 modules/nitro-epub/src/specs/Epub.nitro.ts create mode 100644 modules/nitro-epub/src/types/EpubChapter.ts create mode 100644 modules/nitro-epub/src/types/EpubExportChapter.ts create mode 100644 modules/nitro-epub/src/types/EpubExportMetadata.ts create mode 100644 modules/nitro-epub/src/types/EpubExportResult.ts create mode 100644 modules/nitro-epub/src/types/EpubNovel.ts rename modules/{nitro-import-epub => nitro-epub}/tsconfig.json (100%) delete mode 100644 modules/nitro-import-epub/README.md delete mode 100644 modules/nitro-import-epub/android/gradle.properties delete mode 100644 modules/nitro-import-epub/cpp/HybridEpub.cpp delete mode 100644 modules/nitro-import-epub/cpp/HybridEpub.hpp delete mode 100644 modules/nitro-import-epub/ios/Bridge.h delete mode 100644 modules/nitro-import-epub/nitro.json delete mode 100644 modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.cpp delete mode 100644 modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.cpp delete mode 100644 modules/nitro-import-epub/src/index.ts delete mode 100644 modules/nitro-import-epub/src/specs/Epub.nitro.ts delete mode 100644 patches/@cd-z__react-native-epub-creator.patch create mode 100644 skills-lock.json diff --git a/.gitignore b/.gitignore index 4801d98e..50cc5f88 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,4 @@ src/generated/**/* android/ !modules/**/android/ ios/ +!modules/**/ios/ diff --git a/modules/nitro-import-epub/.gitignore b/modules/nitro-epub/.gitignore similarity index 100% rename from modules/nitro-import-epub/.gitignore rename to modules/nitro-epub/.gitignore diff --git a/modules/nitro-import-epub/.watchmanconfig b/modules/nitro-epub/.watchmanconfig similarity index 100% rename from modules/nitro-import-epub/.watchmanconfig rename to modules/nitro-epub/.watchmanconfig diff --git a/modules/nitro-import-epub/NitroImportEpub.podspec b/modules/nitro-epub/NitroEpub.podspec similarity index 84% rename from modules/nitro-import-epub/NitroImportEpub.podspec rename to modules/nitro-epub/NitroEpub.podspec index 14e427dc..876c0cf9 100644 --- a/modules/nitro-import-epub/NitroImportEpub.podspec +++ b/modules/nitro-epub/NitroEpub.podspec @@ -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 diff --git a/modules/nitro-epub/README.md b/modules/nitro-epub/README.md new file mode 100644 index 00000000..6163e464 --- /dev/null +++ b/modules/nitro-epub/README.md @@ -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. diff --git a/modules/nitro-import-epub/android/CMakeLists.txt b/modules/nitro-epub/android/CMakeLists.txt similarity index 60% rename from modules/nitro-import-epub/android/CMakeLists.txt rename to modules/nitro-epub/android/CMakeLists.txt index 7aeb1a42..b0ba51ed 100644 --- a/modules/nitro-import-epub/android/CMakeLists.txt +++ b/modules/nitro-epub/android/CMakeLists.txt @@ -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 ) diff --git a/modules/nitro-import-epub/android/build.gradle b/modules/nitro-epub/android/build.gradle similarity index 93% rename from modules/nitro-import-epub/android/build.gradle rename to modules/nitro-epub/android/build.gradle index 83106a13..5d64ef4b 100644 --- a/modules/nitro-import-epub/android/build.gradle +++ b/modules/nitro-epub/android/build.gradle @@ -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") } - diff --git a/modules/nitro-import-epub/android/fix-prefab.gradle b/modules/nitro-epub/android/fix-prefab.gradle similarity index 100% rename from modules/nitro-import-epub/android/fix-prefab.gradle rename to modules/nitro-epub/android/fix-prefab.gradle diff --git a/modules/nitro-epub/android/gradle.properties b/modules/nitro-epub/android/gradle.properties new file mode 100644 index 00000000..c5eb791c --- /dev/null +++ b/modules/nitro-epub/android/gradle.properties @@ -0,0 +1,5 @@ +NitroEpub_kotlinVersion=2.1.21 +NitroEpub_minSdkVersion=23 +NitroEpub_targetSdkVersion=36 +NitroEpub_compileSdkVersion=36 +NitroEpub_ndkVersion=29.0.14206865 diff --git a/modules/nitro-import-epub/android/src/main/AndroidManifest.xml b/modules/nitro-epub/android/src/main/AndroidManifest.xml similarity index 100% rename from modules/nitro-import-epub/android/src/main/AndroidManifest.xml rename to modules/nitro-epub/android/src/main/AndroidManifest.xml diff --git a/modules/nitro-import-epub/android/src/main/cpp/cpp-adapter.cpp b/modules/nitro-epub/android/src/main/cpp/cpp-adapter.cpp similarity index 61% rename from modules/nitro-import-epub/android/src/main/cpp/cpp-adapter.cpp rename to modules/nitro-epub/android/src/main/cpp/cpp-adapter.cpp index 73cbae30..b3366e7b 100644 --- a/modules/nitro-import-epub/android/src/main/cpp/cpp-adapter.cpp +++ b/modules/nitro-epub/android/src/main/cpp/cpp-adapter.cpp @@ -1,9 +1,9 @@ #include #include -#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(); }); } diff --git a/modules/nitro-import-epub/android/src/main/java/com/margelo/nitro/nitroimportepub/NitroImportEpubPackage.kt b/modules/nitro-epub/android/src/main/java/com/margelo/nitro/nitroepub/NitroEpubPackage.kt similarity index 76% rename from modules/nitro-import-epub/android/src/main/java/com/margelo/nitro/nitroimportepub/NitroImportEpubPackage.kt rename to modules/nitro-epub/android/src/main/java/com/margelo/nitro/nitroepub/NitroEpubPackage.kt index a7a3fe52..d33d27cb 100644 --- a/modules/nitro-import-epub/android/src/main/java/com/margelo/nitro/nitroimportepub/NitroImportEpubPackage.kt +++ b/modules/nitro-epub/android/src/main/java/com/margelo/nitro/nitroepub/NitroEpubPackage.kt @@ -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() } } } diff --git a/modules/nitro-import-epub/babel.config.js b/modules/nitro-epub/babel.config.js similarity index 100% rename from modules/nitro-import-epub/babel.config.js rename to modules/nitro-epub/babel.config.js diff --git a/modules/nitro-epub/cpp/HybridEpub.cpp b/modules/nitro-epub/cpp/HybridEpub.cpp new file mode 100644 index 00000000..6c23c0d6 --- /dev/null +++ b/modules/nitro-epub/cpp/HybridEpub.cpp @@ -0,0 +1,70 @@ +#include "HybridEpub.hpp" +#include "export/EpubExporter.hpp" +#include "import/EpubParser.hpp" + +namespace margelo::nitro::nitroepub { + +std::shared_ptr> HybridEpub::parseNovelAndChapters( + const std::string& epubDirPath) { + return Promise::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> HybridEpub::exportEpub( + const EpubExportMetadata& metadata, + const std::vector& chapters, + const std::string& outputPath) { + return Promise::async( + [metadata, chapters, outputPath]() { + EpubArchiveMetadata archiveMetadata{ + metadata.title, + metadata.language, + metadata.coverPath, + metadata.description, + metadata.author, + metadata.bookId, + metadata.stylesheet, + metadata.javascript, + }; + std::vector 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(result.chapterCount)); + }); +} + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/HybridEpub.hpp b/modules/nitro-epub/cpp/HybridEpub.hpp new file mode 100644 index 00000000..160b001f --- /dev/null +++ b/modules/nitro-epub/cpp/HybridEpub.hpp @@ -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> parseNovelAndChapters( + const std::string& epubDirPath) override; + std::shared_ptr> exportEpub( + const EpubExportMetadata& metadata, + const std::vector& chapters, + const std::string& outputPath) override; +}; + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/EpubAsset.hpp b/modules/nitro-epub/cpp/export/EpubAsset.hpp new file mode 100644 index 00000000..cf0b1bb3 --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubAsset.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace margelo::nitro::nitroepub { + +struct EpubAsset { + std::string id; + std::string archivePath; + std::string mediaType; +}; + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/EpubContentProcessor.cpp b/modules/nitro-epub/cpp/export/EpubContentProcessor.cpp new file mode 100644 index 00000000..68de0eb2 --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubContentProcessor.cpp @@ -0,0 +1,261 @@ +#include "EpubContentProcessor.hpp" + +#include "../pugixml.hpp" + +#include +#include +#include +#include +#include +#include +#include + +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(input), + std::istreambuf_iterator()); +} + +std::string lowercase(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](char character) { + return static_cast( + std::tolower(static_cast(character))); + }); + return value; +} + +int hexValue(char character) { + if (character >= '0' && character <= '9') { + return character - '0'; + } + character = static_cast(std::tolower( + static_cast(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((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"(]*>)", std::regex::icase), ""); + const std::regex bodyPattern( + R"(]*>([\s\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(match.position()) - + cursor); + result += "<" + match[1].str() + match[2].str() + " />"; + cursor = static_cast(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 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(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(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& knownAssets, + std::vector& 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 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& knownAssets, + std::vector& assets) { + std::string body = escapeUnsupportedNamedEntities( + makeVoidElementsXmlCompatible(extractBody(readTextFile(chapter.htmlPath)))); + pugi::xml_document document; + const std::string wrapped = "" + body + ""; + 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 diff --git a/modules/nitro-epub/cpp/export/EpubContentProcessor.hpp b/modules/nitro-epub/cpp/export/EpubContentProcessor.hpp new file mode 100644 index 00000000..30195974 --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubContentProcessor.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "EpubAsset.hpp" +#include "EpubExporter.hpp" +#include "ZipWriter.hpp" + +#include +#include +#include +#include + +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& knownAssets, + std::vector& assets); + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/EpubDocuments.cpp b/modules/nitro-epub/cpp/export/EpubDocuments.cpp new file mode 100644 index 00000000..221b760f --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubDocuments.cpp @@ -0,0 +1,217 @@ +#include "EpubDocuments.hpp" + +#include +#include +#include +#include + +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"( + + + + + +)"; +} + +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"( + )" + : ""; + const std::string onLoad = + hasJavaScript ? " onload=\"fnEpub()\"" : ""; + return R"( + + + + + )" + + title + + R"( + )" + + script + R"( + + + )" + body + R"( + + +)"; +} + +std::string navigationDocument( + const EpubArchiveMetadata& metadata, + const std::vector& chapters) { + std::ostringstream items; + for (std::size_t index = 0; index < chapters.size(); ++index) { + items << "
  • " + << xmlEscape(chapters[index].title) << "
  • \n"; + } + return R"( + + + )" + + xmlEscape(metadata.title) + R"( + + + + +)"; +} + +std::string ncxDocument( + const EpubArchiveMetadata& metadata, + const std::vector& chapters) { + std::ostringstream points; + for (std::size_t index = 0; index < chapters.size(); ++index) { + points << " \n" + << " " + << xmlEscape(chapters[index].title) << "\n" + << " \n" + << " \n"; + } + return R"( + + + )" + + xmlEscape(metadata.title) + R"( + +)" + points.str() + + R"( + +)"; +} + +std::string packageDocument( + const EpubArchiveMetadata& metadata, + const std::vector& chapters, + const std::vector& assets, + const EpubAsset* cover, + bool hasJavaScript) { + std::ostringstream manifest; + manifest << " \n" + << " \n" + << " \n"; + if (hasJavaScript) { + manifest << " \n"; + } + if (cover != nullptr) { + manifest << " archivePath.substr(5)) + << "\" media-type=\"" << cover->mediaType + << "\" properties=\"cover-image\"/>\n"; + } + for (const EpubAsset& asset : assets) { + manifest << " \n"; + } + for (std::size_t index = 0; index < chapters.size(); ++index) { + manifest << " \n"; + } + + std::ostringstream spine; + for (std::size_t index = 0; index < chapters.size(); ++index) { + spine << " \n"; + } + + return R"( + + + )" + + xmlEscape(metadata.bookId) + R"( + )" + + xmlEscape(metadata.title) + R"( + )" + + xmlEscape(metadata.language) + R"( + )" + + xmlEscape(metadata.author) + R"( + )" + + xmlEscape(metadata.description) + R"( + )" + + modifiedTimestamp() + R"( + + +)" + manifest.str() + + R"( + +)" + spine.str() + + R"( + +)"; +} + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/EpubDocuments.hpp b/modules/nitro-epub/cpp/export/EpubDocuments.hpp new file mode 100644 index 00000000..205ffdf2 --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubDocuments.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "EpubAsset.hpp" +#include "EpubExporter.hpp" + +#include +#include + +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& chapters); +std::string ncxDocument( + const EpubArchiveMetadata& metadata, + const std::vector& chapters); +std::string packageDocument( + const EpubArchiveMetadata& metadata, + const std::vector& chapters, + const std::vector& assets, + const EpubAsset* cover, + bool hasJavaScript); + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/EpubExporter.cpp b/modules/nitro-epub/cpp/export/EpubExporter.cpp new file mode 100644 index 00000000..cb001eac --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubExporter.cpp @@ -0,0 +1,102 @@ +#include "EpubExporter.hpp" + +#include "EpubAsset.hpp" +#include "EpubContentProcessor.hpp" +#include "EpubDocuments.hpp" +#include "ZipWriter.hpp" + +#include +#include +#include + +namespace margelo::nitro::nitroepub { + +EpubArchiveResult exportEpubArchive( + const EpubArchiveMetadata& metadata, + const std::vector& 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 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 assets; + std::unordered_map 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 diff --git a/modules/nitro-epub/cpp/export/EpubExporter.hpp b/modules/nitro-epub/cpp/export/EpubExporter.hpp new file mode 100644 index 00000000..fa32028a --- /dev/null +++ b/modules/nitro-epub/cpp/export/EpubExporter.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +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& chapters, + const std::string& outputPath); + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/ZipWriter.cpp b/modules/nitro-epub/cpp/export/ZipWriter.cpp new file mode 100644 index 00000000..70bbe559 --- /dev/null +++ b/modules/nitro-epub/cpp/export/ZipWriter.cpp @@ -0,0 +1,213 @@ +#include "ZipWriter.hpp" + +#include +#include +#include +#include +#include + +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::max()) { + throw std::runtime_error(std::string("EPUB exceeds ZIP32 ") + description); + } + return static_cast(value); +} + +std::uint16_t checked16(std::size_t value, const char* description) { + if (value > std::numeric_limits::max()) { + throw std::runtime_error(std::string("EPUB exceeds ZIP16 ") + description); + } + return static_cast(value); +} + +std::vector deflateRaw(const std::vector& 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 output( + std::max(128, compressBound(input.size()))); + stream.next_in = const_cast( + reinterpret_cast(input.data())); + stream.avail_in = checked32(input.size(), "entry size"); + stream.next_out = reinterpret_cast(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(content.begin(), content.end()), false); +} + +void ZipWriter::addDeflated(const std::string& archivePath, + const std::string& content) { + add(archivePath, + std::vector(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 content( + (std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + add(archivePath, content, compress); +} + +void ZipWriter::add(const std::string& archivePath, + const std::vector& 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( + crc32(0, reinterpret_cast(content.data()), + checked32(content.size(), "entry size"))); + const auto offset = checked32(static_cast(output_.tellp()), + "local header offset"); + const auto pathLength = checked16(archivePath.size(), "entry name length"); + const auto method = static_cast(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(compressed.data()), + static_cast(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(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(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(value & 0xff), + static_cast((value >> 8) & 0xff), + }; + output_.write(bytes, sizeof(bytes)); +} + +void ZipWriter::write32(std::uint32_t value) { + const char bytes[] = { + static_cast(value & 0xff), + static_cast((value >> 8) & 0xff), + static_cast((value >> 16) & 0xff), + static_cast((value >> 24) & 0xff), + }; + output_.write(bytes, sizeof(bytes)); +} + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-epub/cpp/export/ZipWriter.hpp b/modules/nitro-epub/cpp/export/ZipWriter.hpp new file mode 100644 index 00000000..b12968d3 --- /dev/null +++ b/modules/nitro-epub/cpp/export/ZipWriter.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +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 entries_; + bool finished_ = false; + + void add(const std::string& archivePath, + const std::vector& content, + bool compress); + void write16(std::uint16_t value); + void write32(std::uint32_t value); +}; + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-import-epub/cpp/EpubParser.cpp b/modules/nitro-epub/cpp/import/EpubParser.cpp similarity index 99% rename from modules/nitro-import-epub/cpp/EpubParser.cpp rename to modules/nitro-epub/cpp/import/EpubParser.cpp index 32133294..d23d2bb8 100644 --- a/modules/nitro-import-epub/cpp/EpubParser.cpp +++ b/modules/nitro-epub/cpp/import/EpubParser.cpp @@ -1,5 +1,5 @@ #include -#include +#include "../pugixml.hpp" #include #include #include @@ -300,4 +300,4 @@ EpubMetadata parseEpub(const std::string epub_path) parse_opf_from_folder(epub_path, opf_path, metadata); return metadata; -} \ No newline at end of file +} diff --git a/modules/nitro-import-epub/cpp/EpubParser.hpp b/modules/nitro-epub/cpp/import/EpubParser.hpp similarity index 100% rename from modules/nitro-import-epub/cpp/EpubParser.hpp rename to modules/nitro-epub/cpp/import/EpubParser.hpp diff --git a/modules/nitro-import-epub/cpp/pugiconfig.hpp b/modules/nitro-epub/cpp/pugiconfig.hpp similarity index 100% rename from modules/nitro-import-epub/cpp/pugiconfig.hpp rename to modules/nitro-epub/cpp/pugiconfig.hpp diff --git a/modules/nitro-import-epub/cpp/pugixml.cpp b/modules/nitro-epub/cpp/pugixml.cpp similarity index 100% rename from modules/nitro-import-epub/cpp/pugixml.cpp rename to modules/nitro-epub/cpp/pugixml.cpp diff --git a/modules/nitro-import-epub/cpp/pugixml.hpp b/modules/nitro-epub/cpp/pugixml.hpp similarity index 100% rename from modules/nitro-import-epub/cpp/pugixml.hpp rename to modules/nitro-epub/cpp/pugixml.hpp diff --git a/modules/nitro-epub/cpp/tests/EpubExporterTest.cpp b/modules/nitro-epub/cpp/tests/EpubExporterTest.cpp new file mode 100644 index 00000000..329a18ff --- /dev/null +++ b/modules/nitro-epub/cpp/tests/EpubExporterTest.cpp @@ -0,0 +1,93 @@ +#include "../export/EpubExporter.hpp" + +#include +#include +#include +#include +#include +#include +#include + +using namespace margelo::nitro::nitroepub; + +namespace { + +std::uint16_t read16(const std::vector& bytes, + std::size_t offset) { + return static_cast(bytes[offset]) | + static_cast(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(png), sizeof(png)); + } + + const std::filesystem::path chapterPath = + fixtureDirectory / "chapter.html"; + { + std::ofstream chapter(chapterPath); + chapter << "

    One & Two

    " + << "" + << "" + << ""; + } + + 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 ", + 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 bytes( + (std::istreambuf_iterator(epub)), + std::istreambuf_iterator()); + 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; +} diff --git a/modules/nitro-epub/nitro.json b/modules/nitro-epub/nitro.json new file mode 100644 index 00000000..d8a0d38b --- /dev/null +++ b/modules/nitro-epub/nitro.json @@ -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" + ] +} diff --git a/modules/nitro-import-epub/nitrogen/generated/.gitattributes b/modules/nitro-epub/nitrogen/generated/.gitattributes similarity index 100% rename from modules/nitro-import-epub/nitrogen/generated/.gitattributes rename to modules/nitro-epub/nitrogen/generated/.gitattributes diff --git a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.cmake b/modules/nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.cmake similarity index 88% rename from modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.cmake rename to modules/nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.cmake index 0c2e5eac..60d825f8 100644 --- a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.cmake +++ b/modules/nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.cmake @@ -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() diff --git a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.gradle b/modules/nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.gradle similarity index 72% rename from modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.gradle rename to modules/nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.gradle index a6d42660..29e7f453 100644 --- a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpub+autolinking.gradle +++ b/modules/nitro-epub/nitrogen/generated/android/NitroEpub+autolinking.gradle @@ -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 { diff --git a/modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.cpp b/modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.cpp new file mode 100644 index 00000000..256f9767 --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.cpp @@ -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 +#include +#include + +#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 { + static_assert(std::is_default_constructible_v, + "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(); + } + ); +} + +} // namespace margelo::nitro::nitroepub diff --git a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.hpp b/modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.hpp similarity index 69% rename from modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.hpp rename to modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.hpp index 964f7e80..53850559 100644 --- a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.hpp +++ b/modules/nitro-epub/nitrogen/generated/android/NitroEpubOnLoad.hpp @@ -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 #include -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 diff --git a/modules/nitro-import-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimportepub/NitroImportEpubOnLoad.kt b/modules/nitro-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroepub/NitroEpubOnLoad.kt similarity index 53% rename from modules/nitro-import-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimportepub/NitroImportEpubOnLoad.kt rename to modules/nitro-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroepub/NitroEpubOnLoad.kt index 4bdb3848..92f896dc 100644 --- a/modules/nitro-import-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimportepub/NitroImportEpubOnLoad.kt +++ b/modules/nitro-epub/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroepub/NitroEpubOnLoad.kt @@ -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 } diff --git a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub+autolinking.rb b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub+autolinking.rb similarity index 88% rename from modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub+autolinking.rb rename to modules/nitro-epub/nitrogen/generated/ios/NitroEpub+autolinking.rb index 69d09736..a528c2f8 100644 --- a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub+autolinking.rb +++ b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub+autolinking.rb @@ -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']) diff --git a/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.cpp b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.cpp new file mode 100644 index 00000000..884fae4e --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.cpp @@ -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 diff --git a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.hpp b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.hpp similarity index 74% rename from modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.hpp rename to modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.hpp index e826676c..2f1918ae 100644 --- a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.hpp +++ b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Bridge.hpp @@ -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 diff --git a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Umbrella.hpp b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Umbrella.hpp similarity index 60% rename from modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Umbrella.hpp rename to modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Umbrella.hpp index f48179db..629c5e7e 100644 --- a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Umbrella.hpp +++ b/modules/nitro-epub/nitrogen/generated/ios/NitroEpub-Swift-Cxx-Umbrella.hpp @@ -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 @@ -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() -#include +#elif __has_include() +#include #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 diff --git a/modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.mm b/modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.mm new file mode 100644 index 00000000..d713e68c --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.mm @@ -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 +#import + +#import + +#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 { + static_assert(std::is_default_constructible_v, + "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(); + } + ); +} + +@end diff --git a/modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.swift b/modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.swift new file mode 100644 index 00000000..ffd50dc9 --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/ios/NitroEpubAutolinking.swift @@ -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 + + +} diff --git a/modules/nitro-import-epub/nitrogen/generated/shared/c++/EpubChapter.hpp b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubChapter.hpp similarity index 87% rename from modules/nitro-import-epub/nitrogen/generated/shared/c++/EpubChapter.hpp rename to modules/nitro-epub/nitrogen/generated/shared/c++/EpubChapter.hpp index 5d49755e..4b109009 100644 --- a/modules/nitro-import-epub/nitrogen/generated/shared/c++/EpubChapter.hpp +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubChapter.hpp @@ -32,7 +32,7 @@ #include -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 final { - static inline margelo::nitro::nitroimportepub::EpubChapter fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + struct JSIConverter 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::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name"))), JSIConverter::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::toJSI(runtime, arg.name)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "path"), JSIConverter::toJSI(runtime, arg.path)); diff --git a/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportChapter.hpp b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportChapter.hpp new file mode 100644 index 00000000..898e4df4 --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportChapter.hpp @@ -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() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +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 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::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "htmlPath"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "novelId"))), + JSIConverter::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::toJSI(runtime, arg.title)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "htmlPath"), JSIConverter::toJSI(runtime, arg.htmlPath)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "novelId"), JSIConverter::toJSI(runtime, arg.novelId)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapterId"), JSIConverter::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::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "htmlPath")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "novelId")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapterId")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportMetadata.hpp b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportMetadata.hpp new file mode 100644 index 00000000..88d24964 --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportMetadata.hpp @@ -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() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +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 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::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "coverPath"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "description"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bookId"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stylesheet"))), + JSIConverter::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::toJSI(runtime, arg.title)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "language"), JSIConverter::toJSI(runtime, arg.language)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "coverPath"), JSIConverter::toJSI(runtime, arg.coverPath)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "description"), JSIConverter::toJSI(runtime, arg.description)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "author"), JSIConverter::toJSI(runtime, arg.author)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "bookId"), JSIConverter::toJSI(runtime, arg.bookId)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "stylesheet"), JSIConverter::toJSI(runtime, arg.stylesheet)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "javascript"), JSIConverter::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::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "title")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "language")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "coverPath")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "description")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bookId")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "stylesheet")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "javascript")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportResult.hpp b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportResult.hpp new file mode 100644 index 00000000..e536a729 --- /dev/null +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubExportResult.hpp @@ -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() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include + +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 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::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "outputPath"))), + JSIConverter::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::toJSI(runtime, arg.outputPath)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapterCount"), JSIConverter::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::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "outputPath")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapterCount")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/modules/nitro-import-epub/nitrogen/generated/shared/c++/EpubNovel.hpp b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubNovel.hpp similarity index 50% rename from modules/nitro-import-epub/nitrogen/generated/shared/c++/EpubNovel.hpp rename to modules/nitro-epub/nitrogen/generated/shared/c++/EpubNovel.hpp index 05317169..5b79d9bd 100644 --- a/modules/nitro-import-epub/nitrogen/generated/shared/c++/EpubNovel.hpp +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/EpubNovel.hpp @@ -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 -#include -#include #include #include "EpubChapter.hpp" #include -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> cover SWIFT_PRIVATE; - std::optional> summary SWIFT_PRIVATE; - std::optional> author SWIFT_PRIVATE; - std::optional> artist SWIFT_PRIVATE; + std::optional cover SWIFT_PRIVATE; + std::optional summary SWIFT_PRIVATE; + std::optional author SWIFT_PRIVATE; + std::optional artist SWIFT_PRIVATE; std::vector chapters SWIFT_PRIVATE; std::vector cssPaths SWIFT_PRIVATE; std::vector imagePaths SWIFT_PRIVATE; public: EpubNovel() = default; - explicit EpubNovel(std::string name, std::optional> cover, std::optional> summary, std::optional> author, std::optional> artist, std::vector chapters, std::vector cssPaths, std::vector imagePaths): name(name), cover(cover), summary(summary), author(author), artist(artist), chapters(chapters), cssPaths(cssPaths), imagePaths(imagePaths) {} + explicit EpubNovel(std::string name, std::optional cover, std::optional summary, std::optional author, std::optional artist, std::vector chapters, std::vector cssPaths, std::vector 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 final { - static inline margelo::nitro::nitroimportepub::EpubNovel fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + struct JSIConverter 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::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name"))), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover"))), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary"))), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author"))), - JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist"))), - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cssPaths"))), JSIConverter>::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::toJSI(runtime, arg.name)); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "cover"), JSIConverter>>::toJSI(runtime, arg.cover)); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "summary"), JSIConverter>>::toJSI(runtime, arg.summary)); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "author"), JSIConverter>>::toJSI(runtime, arg.author)); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "artist"), JSIConverter>>::toJSI(runtime, arg.artist)); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapters"), JSIConverter>::toJSI(runtime, arg.chapters)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "cover"), JSIConverter>::toJSI(runtime, arg.cover)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "summary"), JSIConverter>::toJSI(runtime, arg.summary)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "author"), JSIConverter>::toJSI(runtime, arg.author)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "artist"), JSIConverter>::toJSI(runtime, arg.artist)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "chapters"), JSIConverter>::toJSI(runtime, arg.chapters)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "cssPaths"), JSIConverter>::toJSI(runtime, arg.cssPaths)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "imagePaths"), JSIConverter>::toJSI(runtime, arg.imagePaths)); return obj; @@ -103,11 +101,11 @@ namespace margelo::nitro { return false; } if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "name")))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover")))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary")))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author")))) return false; - if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist")))) return false; - if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cover")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "summary")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "author")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "artist")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "chapters")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "cssPaths")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "imagePaths")))) return false; return true; diff --git a/modules/nitro-import-epub/nitrogen/generated/shared/c++/HybridEpubSpec.cpp b/modules/nitro-epub/nitrogen/generated/shared/c++/HybridEpubSpec.cpp similarity index 76% rename from modules/nitro-import-epub/nitrogen/generated/shared/c++/HybridEpubSpec.cpp rename to modules/nitro-epub/nitrogen/generated/shared/c++/HybridEpubSpec.cpp index 6904c3bb..1d7fe42a 100644 --- a/modules/nitro-import-epub/nitrogen/generated/shared/c++/HybridEpubSpec.cpp +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/HybridEpubSpec.cpp @@ -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 diff --git a/modules/nitro-import-epub/nitrogen/generated/shared/c++/HybridEpubSpec.hpp b/modules/nitro-epub/nitrogen/generated/shared/c++/HybridEpubSpec.hpp similarity index 57% rename from modules/nitro-import-epub/nitrogen/generated/shared/c++/HybridEpubSpec.hpp rename to modules/nitro-epub/nitrogen/generated/shared/c++/HybridEpubSpec.hpp index f3362185..cee7af8f 100644 --- a/modules/nitro-import-epub/nitrogen/generated/shared/c++/HybridEpubSpec.hpp +++ b/modules/nitro-epub/nitrogen/generated/shared/c++/HybridEpubSpec.hpp @@ -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 #include +#include "EpubExportResult.hpp" +#include "EpubExportMetadata.hpp" +#include "EpubExportChapter.hpp" +#include -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> parseNovelAndChapters(const std::string& epubDirPath) = 0; + virtual std::shared_ptr> exportEpub(const EpubExportMetadata& metadata, const std::vector& 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 diff --git a/modules/nitro-import-epub/package.json b/modules/nitro-epub/package.json similarity index 96% rename from modules/nitro-import-epub/package.json rename to modules/nitro-epub/package.json index bef3528e..21cf730f 100644 --- a/modules/nitro-import-epub/package.json +++ b/modules/nitro-epub/package.json @@ -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", diff --git a/modules/nitro-import-epub/react-native.config.js b/modules/nitro-epub/react-native.config.js similarity index 100% rename from modules/nitro-import-epub/react-native.config.js rename to modules/nitro-epub/react-native.config.js diff --git a/modules/nitro-epub/src/index.ts b/modules/nitro-epub/src/index.ts new file mode 100644 index 00000000..c2fa21b2 --- /dev/null +++ b/modules/nitro-epub/src/index.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') + +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' diff --git a/modules/nitro-epub/src/specs/Epub.nitro.ts b/modules/nitro-epub/src/specs/Epub.nitro.ts new file mode 100644 index 00000000..f45436fa --- /dev/null +++ b/modules/nitro-epub/src/specs/Epub.nitro.ts @@ -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 + + /** + * Creates an EPUB archive from downloaded chapter files. + */ + exportEpub( + metadata: EpubExportMetadata, + chapters: EpubExportChapter[], + outputPath: string + ): Promise +} diff --git a/modules/nitro-epub/src/types/EpubChapter.ts b/modules/nitro-epub/src/types/EpubChapter.ts new file mode 100644 index 00000000..65e3bbd0 --- /dev/null +++ b/modules/nitro-epub/src/types/EpubChapter.ts @@ -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 +} diff --git a/modules/nitro-epub/src/types/EpubExportChapter.ts b/modules/nitro-epub/src/types/EpubExportChapter.ts new file mode 100644 index 00000000..05f4fd1f --- /dev/null +++ b/modules/nitro-epub/src/types/EpubExportChapter.ts @@ -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 +} diff --git a/modules/nitro-epub/src/types/EpubExportMetadata.ts b/modules/nitro-epub/src/types/EpubExportMetadata.ts new file mode 100644 index 00000000..77a54d66 --- /dev/null +++ b/modules/nitro-epub/src/types/EpubExportMetadata.ts @@ -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 +} diff --git a/modules/nitro-epub/src/types/EpubExportResult.ts b/modules/nitro-epub/src/types/EpubExportResult.ts new file mode 100644 index 00000000..c78f3d21 --- /dev/null +++ b/modules/nitro-epub/src/types/EpubExportResult.ts @@ -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 +} diff --git a/modules/nitro-epub/src/types/EpubNovel.ts b/modules/nitro-epub/src/types/EpubNovel.ts new file mode 100644 index 00000000..7b044834 --- /dev/null +++ b/modules/nitro-epub/src/types/EpubNovel.ts @@ -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[] +} diff --git a/modules/nitro-import-epub/tsconfig.json b/modules/nitro-epub/tsconfig.json similarity index 100% rename from modules/nitro-import-epub/tsconfig.json rename to modules/nitro-epub/tsconfig.json diff --git a/modules/nitro-import-epub/README.md b/modules/nitro-import-epub/README.md deleted file mode 100644 index a0b6f8d0..00000000 --- a/modules/nitro-import-epub/README.md +++ /dev/null @@ -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`. diff --git a/modules/nitro-import-epub/android/gradle.properties b/modules/nitro-import-epub/android/gradle.properties deleted file mode 100644 index d7f4ba2f..00000000 --- a/modules/nitro-import-epub/android/gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -NitroImportEpub_kotlinVersion=2.1.21 -NitroImportEpub_minSdkVersion=23 -NitroImportEpub_targetSdkVersion=36 -NitroImportEpub_compileSdkVersion=36 -NitroImportEpub_ndkVersion=29.0.14206865 diff --git a/modules/nitro-import-epub/cpp/HybridEpub.cpp b/modules/nitro-import-epub/cpp/HybridEpub.cpp deleted file mode 100644 index 9ddf8051..00000000 --- a/modules/nitro-import-epub/cpp/HybridEpub.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "HybridEpub.hpp" -#include "EpubParser.hpp" -#include - -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(metadata.cover)); - result.summary = metadata.summary.empty() - ? std::nullopt - : std::optional(std::variant(metadata.summary)); - result.author = metadata.author.empty() - ? std::nullopt - : std::optional(std::variant(metadata.author)); - result.artist = metadata.artist.empty() - ? std::nullopt - : std::optional(std::variant(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 { - return std::make_shared(); - } - ); -} - -} // namespace margelo::nitro::nitroimportepub diff --git a/modules/nitro-import-epub/cpp/HybridEpub.hpp b/modules/nitro-import-epub/cpp/HybridEpub.hpp deleted file mode 100644 index 66c47c72..00000000 --- a/modules/nitro-import-epub/cpp/HybridEpub.hpp +++ /dev/null @@ -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 diff --git a/modules/nitro-import-epub/ios/Bridge.h b/modules/nitro-import-epub/ios/Bridge.h deleted file mode 100644 index 684aedb0..00000000 --- a/modules/nitro-import-epub/ios/Bridge.h +++ /dev/null @@ -1,8 +0,0 @@ -// -// Bridge.h -// NitroImportEpub -// -// Created by Marc Rousavy on 22.07.24. -// - -#pragma once diff --git a/modules/nitro-import-epub/nitro.json b/modules/nitro-import-epub/nitro.json deleted file mode 100644 index f1332db3..00000000 --- a/modules/nitro-import-epub/nitro.json +++ /dev/null @@ -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" - ] -} diff --git a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.cpp b/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.cpp deleted file mode 100644 index d20bb5a1..00000000 --- a/modules/nitro-import-epub/nitrogen/generated/android/NitroImportEpubOnLoad.cpp +++ /dev/null @@ -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 -#include -#include - - - -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 diff --git a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.cpp b/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.cpp deleted file mode 100644 index 7de980f2..00000000 --- a/modules/nitro-import-epub/nitrogen/generated/ios/NitroImportEpub-Swift-Cxx-Bridge.cpp +++ /dev/null @@ -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 diff --git a/modules/nitro-import-epub/src/index.ts b/modules/nitro-import-epub/src/index.ts deleted file mode 100644 index d70ebfca..00000000 --- a/modules/nitro-import-epub/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { Epub, EpubNovel, EpubChapter } from './specs/Epub.nitro'; diff --git a/modules/nitro-import-epub/src/specs/Epub.nitro.ts b/modules/nitro-import-epub/src/specs/Epub.nitro.ts deleted file mode 100644 index 7ec17bca..00000000 --- a/modules/nitro-import-epub/src/specs/Epub.nitro.ts +++ /dev/null @@ -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; -} diff --git a/package.json b/package.json index da720e11..130a7ea7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/@cd-z__react-native-epub-creator.patch b/patches/@cd-z__react-native-epub-creator.patch deleted file mode 100644 index 8bf37994..00000000 --- a/patches/@cd-z__react-native-epub-creator.patch +++ /dev/null @@ -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" - } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd4fd2b5..69068684 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e29b7153..763c1c77 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,6 +29,3 @@ onlyBuiltDependencies: - better-sqlite3 - esbuild - protobufjs - -patchedDependencies: - '@cd-z/react-native-epub-creator': patches/@cd-z__react-native-epub-creator.patch diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..305d3ded --- /dev/null +++ b/skills-lock.json @@ -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" + } + } +} diff --git a/src/screens/novel/components/ExportEpubModal.tsx b/src/screens/novel/components/ExportEpubModal.tsx index 08c3271c..54385bf7 100644 --- a/src/screens/novel/components/ExportEpubModal.tsx +++ b/src/screens/novel/components/ExportEpubModal.tsx @@ -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 = ({ isVisible, + defaultFileName, onSubmit: onSubmitProp, hideModal, }) => { @@ -31,6 +38,7 @@ const ExportEpubModal: React.FC = ({ } = 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 = ({ const onDismiss = () => { hideModal(); setUri(epubLocation); + setFileName(defaultFileName); exportAll.setTrue(); setStartChapter(''); setEndChapter(''); @@ -77,7 +86,7 @@ const ExportEpubModal: React.FC = ({ 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 = ({ /> } /> + } + style={styles.fileNameInput} + /> 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 = ({ novel, iconComponent: IconComponent, @@ -55,7 +66,7 @@ const ExportNovelAsEpubButton: React.FC = ({ 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 = ({ 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 = ({ } 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 = ({ const exportNovelAsEpub = async ( destinationUri: string, + fileName: string, startChapter?: number, endChapter?: number, ) => { @@ -120,7 +132,7 @@ const ExportNovelAsEpubButton: React.FC = ({ return; } - let epub: EpubBuilder | undefined; + let tempEpubPath: string | undefined; try { const chapters = await getNovelDownloadedChapters( @@ -134,82 +146,53 @@ const ExportNovelAsEpubButton: React.FC = ({ 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( - `]*>.*?${escapedPath}.*?`, - 'gs', - ); - - chapterContent = chapterContent.replace(figureRegex, ''); - - const imgRegex = new RegExp( - `]*${escapedPath}[^>]*\\/?>`, - 'g', - ); - - chapterContent = chapterContent.replace(imgRegex, ''); - } - } - - await epub.addChapter({ - title: - chapter.name?.trim() || `Chapter ${chapter.chapterNumber || i}`, - fileName: `Chapter${i}`, - htmlBody: `${chapterContent}`, - }); - - 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 = ({ 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 = ({ diff --git a/src/services/epub/import.ts b/src/services/epub/import.ts index cd66b303..78ab6405 100644 --- a/src/services/epub/import.ts +++ b/src/services/epub/import.ts @@ -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'); - const novel = epub.parseNovelAndChapters(epubDirPath); + const novel = await epub.parseNovelAndChapters(epubDirPath); if (!novel.name) { novel.name = filename.replace('.epub', '') || 'Untitled'; } diff --git a/strings/languages/en/strings.json b/strings/languages/en/strings.json index 0131df36..bfb08f5d 100644 --- a/strings/languages/en/strings.json +++ b/strings/languages/en/strings.json @@ -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", diff --git a/strings/types/index.ts b/strings/types/index.ts index 1423ca0f..16c491a1 100644 --- a/strings/types/index.ts +++ b/strings/types/index.ts @@ -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';