Extract typefactory converter from CLI

This commit is contained in:
deeaitch committed 2026-08-28 00:07:16 -04:00
1 parent 2535953b8b
commit 28c9752f18
5 files changed
+338 -314

No files matched your search

+297
View File
@@ -0,0 +1,297 @@
#include "converter.h"
#include <array>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <unordered_map>
#include <utility>
#include <vector>
#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<char> (ch));
break;
}
}
return out;
}
struct Quote {
std::string meta;
std::vector<std::string> text_lines;
};
struct ParseContext {
std::vector<std::string> order;
std::unordered_map<std::string, std::vector<Quote>> by_title;
std::unordered_map<std::string, std::size_t> seen;
std::string cur_title;
std::string cur_meta;
std::vector<std::string> 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<Stage, 3> 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<ILineHandler>;
using HandlerMap = std::unordered_map<Stage, HandlerPtr>;
static TypeFactory<Stage, ILineHandler> build_factory() {
TypeFactory<Stage, ILineHandler> factory;
factory.registerType<TitleHandler> (Stage::Title);
factory.registerType<MetaHandler> (Stage::Meta);
factory.registerType<BodyHandler> (Stage::Body);
return factory;
}
static HandlerMap build_handlers_cache (
const TypeFactory<Stage, ILineHandler> &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<Impl>()) {
}
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<Stage, ILineHandler> 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 &quote : 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;
}
+29
View File
@@ -0,0 +1,29 @@
#ifndef CONVERTER_H
#define CONVERTER_H
#include <memory>
#include <string>
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> impl_;
int error_code_ = 0;
};
#endif // CONVERTER_H
+8 -314
View File
@@ -1,231 +1,8 @@
#include <array>
#include <cerrno>
#include <cstring>
#include <fstream>
#include <getopt.h>
#include <iostream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#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<char> (ch));
break;
}
}
return out;
}
// ------------------------------------------------------------
// Data model
// ------------------------------------------------------------
struct Quote {
std::string meta;
std::vector<std::string> text_lines;
};
struct ParseContext {
std::vector<std::string> order;
std::unordered_map<std::string, std::vector<Quote>> by_title;
std::unordered_map<std::string, std::size_t> seen;
std::string cur_title;
std::string cur_meta;
std::vector<std::string> 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<Stage, 3> 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<ILineHandler>;
using HandlerMap = std::unordered_map<Stage, HandlerPtr>;
static TypeFactory<Stage, ILineHandler> build_factory() {
TypeFactory<Stage, ILineHandler> f;
f.registerType<TitleHandler> (Stage::Title);
f.registerType<MetaHandler> (Stage::Meta);
f.registerType<BodyHandler> (Stage::Body);
return f;
}
static HandlerMap build_handlers_cache (
const TypeFactory<Stage, ILineHandler> &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 <file> --output <file>\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<Stage, ILineHandler> 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;
}
+2
View File
@@ -4,7 +4,9 @@ CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
converter.cpp \
main.cpp
HEADERS += \
converter.h \
typefactory.h