diff --git a/CMakeLists.txt b/CMakeLists.txt index c85cc56..40aec88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,8 @@ function(kindle2latex_enable_warnings target) endfunction() add_executable(kindle2latex_typefactory + typefactory/converter.cpp + typefactory/converter.h typefactory/main.cpp typefactory/typefactory.h ) diff --git a/typefactory/converter.cpp b/typefactory/converter.cpp new file mode 100644 index 0000000..25a5917 --- /dev/null +++ b/typefactory/converter.cpp @@ -0,0 +1,297 @@ +#include "converter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "typefactory.h" + +static std::string rstrip_cr (std::string s) { + if (!s.empty() && s.back() == '\r') + s.pop_back(); + return s; +} + +static std::string latex_escape (const std::string &s) { + std::string out; + out.reserve (s.size() + s.size() / 8); + for (unsigned char ch : s) { + switch (ch) { + case '\\': + out += "\\textbackslash{}"; + break; + case '{': + out += "\\{"; + break; + case '}': + out += "\\}"; + break; + case '%': + out += "\\%"; + break; + case '$': + out += "\\$"; + break; + case '#': + out += "\\#"; + break; + case '&': + out += "\\&"; + break; + case '_': + out += "\\_"; + break; + case '^': + out += "\\textasciicircum{}"; + break; + case '~': + out += "\\textasciitilde{}"; + break; + default: + out.push_back (static_cast (ch)); + break; + } + } + return out; +} + +struct Quote { + std::string meta; + std::vector text_lines; +}; + +struct ParseContext { + std::vector order; + std::unordered_map> by_title; + std::unordered_map seen; + + std::string cur_title; + std::string cur_meta; + std::vector cur_text; + + void start_title (std::string title) { + cur_title = std::move (title); + cur_meta.clear(); + cur_text.clear(); + } + + void set_meta (std::string meta) { + cur_meta = std::move (meta); + } + + void add_text (std::string line) { + cur_text.push_back (std::move (line)); + } + + void finalize_quote() { + if (cur_title.empty() || cur_meta.empty()) { + cur_meta.clear(); + cur_text.clear(); + return; + } + + if (seen.find (cur_title) == seen.end()) { + seen.emplace (cur_title, order.size()); + order.push_back (cur_title); + } + + by_title[cur_title].push_back (Quote{cur_meta, cur_text}); + + cur_meta.clear(); + cur_text.clear(); + } +}; + +enum class Stage { Title, Meta, Body }; + +static constexpr std::array kAllStages = { + Stage::Title, Stage::Meta, Stage::Body +}; + +struct ILineHandler { + virtual ~ILineHandler() = default; + + virtual void handle (const std::string &line, + ParseContext &ctx, + Stage &next) = 0; +}; + +struct TitleHandler final : ILineHandler { + void handle (const std::string &line, + ParseContext &ctx, + Stage &next) override { + if (line.empty()) { + next = Stage::Title; + return; + } + + ctx.start_title (line); + next = Stage::Meta; + } +}; + +struct MetaHandler final : ILineHandler { + void handle (const std::string &line, + ParseContext &ctx, + Stage &next) override { + ctx.set_meta (line); + next = Stage::Body; + } +}; + +struct BodyHandler final : ILineHandler { + void handle (const std::string &line, + ParseContext &ctx, + Stage &next) override { + if (line == "==========") { + ctx.finalize_quote(); + next = Stage::Title; + return; + } + + ctx.add_text (line); + next = Stage::Body; + } +}; + +using HandlerPtr = std::shared_ptr; +using HandlerMap = std::unordered_map; + +static TypeFactory build_factory() { + TypeFactory factory; + + factory.registerType (Stage::Title); + factory.registerType (Stage::Meta); + factory.registerType (Stage::Body); + + return factory; +} + +static HandlerMap build_handlers_cache ( + const TypeFactory &factory) { + + HandlerMap handlers; + handlers.reserve (kAllStages.size()); + + for (Stage stage : kAllStages) { + try { + auto handler = factory.create (stage); + if (!handler) + throw std::runtime_error ("Factory returned null handler"); + handlers.emplace (stage, std::move (handler)); + } catch (const std::out_of_range &) { + throw std::runtime_error ("Missing handler registration for Stage"); + } + } + + if (handlers.size() != kAllStages.size()) + throw std::runtime_error ("Handler cache size mismatch"); + + return handlers; +} + +struct Converter::Impl { + ParseContext context; +}; + +Converter::Converter (std::string input_file_name) + : input_file_name_ (std::move (input_file_name)) + , impl_ (std::make_unique()) { +} + +Converter::~Converter() = default; + +int Converter::error_code() const noexcept { + return error_code_; +} + +bool Converter::parse() { + std::ifstream input (input_file_name_); + if (!input.is_open()) { + std::cerr << "Failed to open input file: " + << input_file_name_ + << " (" << std::strerror (errno) << ")\n"; + error_code_ = 2; + return false; + } + + TypeFactory factory = build_factory(); + + HandlerMap handlers; + try { + handlers = build_handlers_cache (factory); + } catch (const std::exception &e) { + std::cerr << "Internal error while building handler cache: " + << e.what() << "\n"; + error_code_ = 4; + return false; + } + + impl_->context = ParseContext{}; + Stage stage = Stage::Title; + + std::string line; + while (std::getline (input, line)) { + line = rstrip_cr (std::move (line)); + + auto it = handlers.find (stage); + if (it == handlers.end() || !it->second) { + std::cerr << "Internal error: missing handler at runtime\n"; + error_code_ = 4; + return false; + } + + Stage next = stage; + it->second->handle (line, impl_->context, next); + stage = next; + } + + impl_->context.finalize_quote(); + error_code_ = 0; + return true; +} + +bool Converter::save (const std::string &output_file_name) { + if (!parse()) + return false; + + std::ofstream output (output_file_name, std::ios::trunc); + if (!output.is_open()) { + std::cerr << "Failed to open output file: " + << output_file_name + << " (" << std::strerror (errno) << ")\n"; + error_code_ = 3; + return false; + } + + const ParseContext &ctx = impl_->context; + for (std::size_t i = 0; i < ctx.order.size(); ++i) { + const auto &title = ctx.order[i]; + output << "\\section {" << latex_escape (title) << "}\n"; + + const auto it = ctx.by_title.find (title); + if (it == ctx.by_title.end()) + continue; + + for (const auto "e : it->second) { + output << " \\subsection {" << latex_escape (quote.meta) << "}\n"; + + for (const auto &text_line : quote.text_lines) + output << " " << latex_escape (text_line) << "\n"; + + output << " \\subsubsection{notes}\n\n"; + } + + if (i + 1 < ctx.order.size()) + output << "\n"; + } + + error_code_ = 0; + return true; +} diff --git a/typefactory/converter.h b/typefactory/converter.h new file mode 100644 index 0000000..2420f19 --- /dev/null +++ b/typefactory/converter.h @@ -0,0 +1,29 @@ +#ifndef CONVERTER_H +#define CONVERTER_H + +#include +#include + +class Converter { +public: + explicit Converter (std::string input_file_name); + ~Converter(); + + Converter (const Converter &) = delete; + Converter &operator= (const Converter &) = delete; + + bool save (const std::string &output_file_name); + [[nodiscard]] int error_code() const noexcept; + +protected: + bool parse(); + +private: + struct Impl; + + std::string input_file_name_; + std::unique_ptr impl_; + int error_code_ = 0; +}; + +#endif // CONVERTER_H diff --git a/typefactory/main.cpp b/typefactory/main.cpp index 61edaf1..8b15481 100644 --- a/typefactory/main.cpp +++ b/typefactory/main.cpp @@ -1,231 +1,8 @@ -#include -#include -#include -#include #include #include -#include #include -#include -#include -#include -#include "typefactory.h" - -// ------------------------------------------------------------ -// Helpers -// ------------------------------------------------------------ - -static std::string rstrip_cr (std::string s) { - if (!s.empty() && s.back() == '\r') - s.pop_back(); - return s; -} - -static std::string latex_escape (const std::string &s) { - std::string out; - out.reserve (s.size() + s.size() / 8); - for (unsigned char ch : s) { - switch (ch) { - case '\\': - out += "\\textbackslash{}"; - break; - case '{': - out += "\\{"; - break; - case '}': - out += "\\}"; - break; - case '%': - out += "\\%"; - break; - case '$': - out += "\\$"; - break; - case '#': - out += "\\#"; - break; - case '&': - out += "\\&"; - break; - case '_': - out += "\\_"; - break; - case '^': - out += "\\textasciicircum{}"; - break; - case '~': - out += "\\textasciitilde{}"; - break; - default: - out.push_back (static_cast (ch)); - break; - } - } - return out; -} - -// ------------------------------------------------------------ -// Data model -// ------------------------------------------------------------ - -struct Quote { - std::string meta; - std::vector text_lines; -}; - -struct ParseContext { - std::vector order; - std::unordered_map> by_title; - std::unordered_map seen; - - std::string cur_title; - std::string cur_meta; - std::vector cur_text; - - void start_title (std::string t) { - cur_title = std::move (t); - cur_meta.clear(); - cur_text.clear(); - } - - void set_meta (std::string m) { - cur_meta = std::move (m); - } - void add_text (std::string line) { - cur_text.push_back (std::move (line)); - } - - void finalize_quote() { - // Ignore incomplete entries - if (cur_title.empty() || cur_meta.empty()) { - cur_meta.clear(); - cur_text.clear(); - return; - } - - // Preserve insertion order of sections - if (seen.find (cur_title) == seen.end()) { - seen.emplace (cur_title, order.size()); - order.push_back (cur_title); - } - - by_title[cur_title].push_back (Quote{cur_meta, cur_text}); - - cur_meta.clear(); - cur_text.clear(); - } -}; - -// ------------------------------------------------------------ -// Parsing stages -// ------------------------------------------------------------ - -enum class Stage { Title, Meta, Body }; - -// Explicit list of all supported stages for runtime validation -static constexpr std::array kAllStages = { - Stage::Title, Stage::Meta, Stage::Body -}; - -struct ILineHandler { - virtual ~ILineHandler() = default; - - // Process a single line and decide the next stage - virtual void handle (const std::string &line, - ParseContext &ctx, - Stage &next) = 0; -}; - -struct TitleHandler final : ILineHandler { - void handle (const std::string &line, - ParseContext &ctx, - Stage &next) override { - - // Skip empty lines between entries - if (line.empty()) { - next = Stage::Title; - return; - } - - ctx.start_title (line); - next = Stage::Meta; - } -}; - -struct MetaHandler final : ILineHandler { - void handle (const std::string &line, - ParseContext &ctx, - Stage &next) override { - - ctx.set_meta (line); - next = Stage::Body; - } -}; - -struct BodyHandler final : ILineHandler { - void handle (const std::string &line, - ParseContext &ctx, - Stage &next) override { - - // Separator marks the end of a clipping block - if (line == "==========") { - ctx.finalize_quote(); - next = Stage::Title; - return; - } - - // Keep body lines as-is (including empty ones) - ctx.add_text (line); - next = Stage::Body; - } -}; - -// ------------------------------------------------------------ -// Factory wiring + handler caching -// ------------------------------------------------------------ - -using HandlerPtr = std::shared_ptr; -using HandlerMap = std::unordered_map; - -static TypeFactory build_factory() { - TypeFactory f; - - f.registerType (Stage::Title); - f.registerType (Stage::Meta); - f.registerType (Stage::Body); - - return f; -} - -static HandlerMap build_handlers_cache ( - const TypeFactory &factory) { - - HandlerMap handlers; - handlers.reserve (kAllStages.size()); - - // Runtime validation: ensure each stage has a registered handler - for (Stage st : kAllStages) { - try { - auto h = factory.create (st); - if (!h) - throw std::runtime_error ("Factory returned null handler"); - handlers.emplace (st, std::move (h)); - } catch (const std::out_of_range &) { - throw std::runtime_error ("Missing handler registration for Stage"); - } - } - - // Extra consistency check - if (handlers.size() != kAllStages.size()) - throw std::runtime_error ("Handler cache size mismatch"); - - return handlers; -} - -// ------------------------------------------------------------ -// CLI handling -// ------------------------------------------------------------ +#include "converter.h" struct CliArgs { std::string input; @@ -237,10 +14,7 @@ static void print_usage (const char *argv0) { << " --input --output \n"; } -static bool parse_args (int argc, - char **argv, - CliArgs &out) { - +static bool parse_args (int argc, char **argv, CliArgs &out) { static option long_opts[] = { {"input", required_argument, nullptr, 'i'}, {"output", required_argument, nullptr, 'o'}, @@ -249,10 +23,7 @@ static bool parse_args (int argc, }; int c = 0; - while ((c = ::getopt_long (argc, argv, - "i:o:h", - long_opts, - nullptr)) != -1) { + while ((c = ::getopt_long (argc, argv, "i:o:h", long_opts, nullptr)) != -1) { switch (c) { case 'i': out.input = optarg; @@ -277,91 +48,14 @@ static bool parse_args (int argc, return true; } -// ------------------------------------------------------------ -// Conversion logic -// ------------------------------------------------------------ - -static int convert (const std::string &in_path, - const std::string &out_path) { - - std::ifstream in (in_path); - if (!in.is_open()) { - std::cerr << "Failed to open input file: " - << in_path - << " (" << std::strerror (errno) << ")\n"; - return 2; - } - - // Build factory and cache handlers once - TypeFactory factory = build_factory(); - - HandlerMap handlers; - try { - handlers = build_handlers_cache (factory); - } catch (const std::exception &e) { - std::cerr << "Internal error while building handler cache: " - << e.what() << "\n"; - return 4; - } - - ParseContext ctx{}; - Stage stage = Stage::Title; - - std::string line; - while (std::getline (in, line)) { - line = rstrip_cr (std::move (line)); - - auto it = handlers.find (stage); - if (it == handlers.end() || !it->second) { - std::cerr << "Internal error: missing handler at runtime\n"; - return 4; - } - - Stage next = stage; - it->second->handle (line, ctx, next); - stage = next; - } - - // Ensure the last entry is flushed if file does not end with separator - ctx.finalize_quote(); - - std::ofstream out (out_path, std::ios::trunc); - if (!out.is_open()) { - std::cerr << "Failed to open output file: " - << out_path - << " (" << std::strerror (errno) << ")\n"; - return 3; - } - - // Generate LaTeX output - for (std::size_t i = 0; i < ctx.order.size(); ++i) { - const auto &title = ctx.order[i]; - out << "\\section {" << latex_escape (title) << "}\n"; - - const auto it = ctx.by_title.find (title); - if (it == ctx.by_title.end()) - continue; - - for (const auto &q : it->second) { - out << " \\subsection {" << latex_escape (q.meta) << "}\n"; - - for (const auto &tl : q.text_lines) - out << " " << latex_escape (tl) << "\n"; - - out << " \\subsubsection{notes}\n\n"; - } - - if (i + 1 < ctx.order.size()) - out << "\n"; - } - - return 0; -} - int main (int argc, char **argv) { CliArgs args; if (!parse_args (argc, argv, args)) return 1; - return convert (args.input, args.output); + Converter converter (args.input); + if (!converter.save (args.output)) + return converter.error_code(); + + return 0; } diff --git a/typefactory/typefactory.pro b/typefactory/typefactory.pro index c1c7248..50e73a2 100644 --- a/typefactory/typefactory.pro +++ b/typefactory/typefactory.pro @@ -4,7 +4,9 @@ CONFIG -= app_bundle CONFIG -= qt SOURCES += \ + converter.cpp \ main.cpp HEADERS += \ + converter.h \ typefactory.h