Extract typefactory converter from CLI
This commit is contained in:
1 parent
2535953b8b
commit
28c9752f18
5 files changed
+338
-314
No files matched your search
@@ -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 "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;
|
||||
}
|
||||
Reference in new issue
Block a user