4 Commits
Author SHA1 Message Date
deeaitch 6ed48022ae Add Git-derived version reporting 2026-08-28 01:30:44 -04:00
deeaitch 7026e246bb Remove legacy FSM implementation 2026-08-28 01:19:21 -04:00
deeaitch 804876656d Add AsciiDoc converter output 2026-08-28 01:16:16 -04:00
deeaitch e5cb11c659 Add Org mode converter output 2026-08-28 01:14:51 -04:00
12 changed files with 253 additions and 684 deletions

No files matched your search

+31 -1
View File
@@ -1,11 +1,34 @@
cmake_minimum_required(VERSION 3.16)
project(kindle2notes LANGUAGES CXX)
project(kindle2notes VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(KINDLE2NOTES_VERSION "${PROJECT_VERSION}")
find_package(Git QUIET)
if(Git_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --always
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE KINDLE2NOTES_GIT_RESULT
OUTPUT_VARIABLE KINDLE2NOTES_GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(KINDLE2NOTES_GIT_RESULT EQUAL 0 AND NOT KINDLE2NOTES_GIT_DESCRIBE STREQUAL "")
if(KINDLE2NOTES_GIT_DESCRIBE MATCHES "^(.+)-0-g[0-9a-f]+$")
set(KINDLE2NOTES_VERSION "${CMAKE_MATCH_1}")
else()
set(KINDLE2NOTES_VERSION "${KINDLE2NOTES_GIT_DESCRIBE}")
endif()
endif()
endif()
message(STATUS "kindle2notes version: ${KINDLE2NOTES_VERSION}")
function(kindle2notes_enable_warnings target)
if(MSVC)
target_compile_options(${target} PRIVATE /W4)
@@ -15,13 +38,20 @@ function(kindle2notes_enable_warnings target)
endfunction()
add_executable(kindle2notes
typefactory/convertasciidoc.cpp
typefactory/convertasciidoc.h
typefactory/convertlatex.cpp
typefactory/convertlatex.h
typefactory/convertmarkdown.cpp
typefactory/convertmarkdown.h
typefactory/convertorg.cpp
typefactory/convertorg.h
typefactory/converter.cpp
typefactory/converter.h
typefactory/main.cpp
typefactory/typefactory.h
)
kindle2notes_enable_warnings(kindle2notes)
target_compile_definitions(kindle2notes PRIVATE
KINDLE2NOTES_VERSION="${KINDLE2NOTES_VERSION}"
)
+71 -1
View File
@@ -7,6 +7,8 @@ Supported output formats:
- LaTeX (`--type=latex`)
- Markdown (`--type=md`)
- Org mode (`--type=org`)
- AsciiDoc (`--type=adoc`)
Highlights are grouped by book title while preserving the order in which
books first appear in the input file.
@@ -63,6 +65,43 @@ Markdown block quote, followed by an empty notes heading:
Empty lines inside highlights are preserved as `>` lines, and Markdown syntax
characters are escaped automatically.
### Org mode
Books and clipping metadata become Org headings. Highlight text is placed in
a quote block followed by an empty notes heading:
```org
* Book Title
** - Your Highlight on Location 123-125 | Added on ...
#+begin_quote
Highlighted text line 1
Highlighted text line 2
#+end_quote
*** Notes
```
### AsciiDoc
Books and clipping metadata become section headings. Highlight text is placed
in a quote block followed by an empty notes section:
```adoc
== Book Title
=== - Your Highlight on Location 123-125 | Added on ...
[quote]
____
Highlighted text line 1
Highlighted text line 2
____
==== Notes
```
## Architecture
Input parsing is implemented as a stage-driven pipeline with one handler for
@@ -92,6 +131,12 @@ The resulting executable is:
build/kindle2notes
```
When the source is built from Git, CMake obtains the version from
`git describe`. A build made exactly from a release tag reports only that tag
(for example, `1.0`). Later commits include their distance from the tag and
the abbreviated commit hash (for example, `1.0-3-gabc1234`). A modified
working tree also receives the `-dirty` suffix.
## Usage
Generate LaTeX:
@@ -112,17 +157,42 @@ Generate Markdown:
--type=md
```
Generate Org mode:
```bash
./build/kindle2notes \
--input "My Clippings.txt" \
--output notes.org \
--type=org
```
Generate AsciiDoc:
```bash
./build/kindle2notes \
--input "My Clippings.txt" \
--output notes.adoc \
--type=adoc
```
Short options are also available:
```bash
./build/kindle2notes -i input.txt -o notes.md -t md
```
Show the version compiled into the executable:
```bash
./build/kindle2notes --version
```
| Argument | Short form | Description |
| --- | --- | --- |
| `--input` | `-i` | Path to the Kindle clippings file |
| `--output` | `-o` | Path to the generated file |
| `--type` | `-t` | Output format: `latex` or `md` |
| `--type` | `-t` | Output format: `latex`, `md`, `org`, or `adoc` |
| `--version` | `-v` | Show the compiled version and exit |
| `--help` | `-h` | Show usage information |
If an unsupported output type is requested, the program prints the list of
-295
View File
@@ -1,295 +0,0 @@
#pragma once
#include <type_traits>
#include <utility>
#include <variant>
/** @brief Missing transition policy. */
enum class missing_transition_policy {
/** @brief Missing transition is considered required by validation. */
strict,
/** @brief Missing transition is allowed (event ignored). */
ignore
};
/** @brief Marker for undefined transition. */
struct no_transition {};
/** @brief Default guard: always allow. */
struct always_allow {
template <typename TContext, typename TFrom, typename TEvent>
bool operator() (TContext &, const TFrom &, const TEvent &) const noexcept {
return true;
}
};
/** @brief Default action: do nothing. */
struct do_nothing {
template <typename TContext, typename TFrom, typename TTo, typename TEvent>
void operator() (TContext &, const TFrom &, const TTo &, const TEvent &) const noexcept {
}
};
/**
* @brief Transition descriptor.
*
* @tparam TTo Destination state type.
* @tparam TGuard Guard functor type.
* @tparam TAction Action functor type.
*/
template <typename TTo, typename TGuard = always_allow, typename TAction = do_nothing>
struct transition_to {
using to_state = TTo;
using guard = TGuard;
using action = TAction;
};
/**
* @brief Transition table mapping: specialize to define transitions.
* Default: no_transition.
*/
template <typename TFrom, typename TEvent>
struct transition {
using type = no_transition;
};
/** @brief Optional mapping to names for logs. */
template <typename T>
struct name_of {
static constexpr const char *value = "?";
};
/**
* @brief Logger interface (default no-op).
*
* You can implement:
* - ignored_transition(from, event)
* - unhandled_transition(from, event) // policy=strict but missing transition at runtime
* - guard_blocked(from, event)
*/
struct null_logger {
void ignored_transition (const char *, const char *) noexcept {}
void unhandled_transition (const char *, const char *) noexcept {}
void guard_blocked (const char *, const char *) noexcept {}
};
/** @brief Hooks: state-only. */
template <typename TState>
struct on_exit_state {
template <typename TContext>
static void call (TContext &, const TState &) noexcept {}
};
template <typename TState>
struct on_entry_state {
template <typename TContext>
static void call (TContext &, const TState &) noexcept {}
};
/** @brief Hooks: event-specific. */
template <typename TState, typename TEvent>
struct on_exit_event {
template <typename TContext>
static void call (TContext &, const TState &, const TEvent &) noexcept {}
};
template <typename TState, typename TEvent>
struct on_entry_event {
template <typename TContext>
static void call (TContext &, const TState &, const TEvent &) noexcept {}
};
/**
* @brief Factory: construct destination state.
*
* IMPORTANT: Now includes TContext in template parameters to avoid templated make<TContext>().
*
* Default: To{}.
* Specialize state_factory<TContext, From, Event, To> as needed.
*/
template <typename TContext, typename TFrom, typename TEvent, typename TTo>
struct state_factory {
static TTo make (TContext &, const TFrom &, const TEvent &) {
return TTo{};
}
};
/**
* @brief Per (State,Event) missing transition policy.
*
* Default is ignore. Mark only required pairs strict.
*/
template <typename TFrom, typename TEvent>
struct missing_transition {
static constexpr missing_transition_policy policy = missing_transition_policy::ignore;
};
/** @brief Helper: is T one of Ts... */
template <typename T, typename... Ts>
struct is_one_of : std::disjunction<std::is_same<T, Ts>...> {};
/** @brief Helper: transition exists? */
template <typename TFrom, typename TEvent>
struct has_transition
: std::bool_constant < !std::is_same_v<typename transition<TFrom, TEvent>::type, no_transition >> {};
/** @brief Factory validation (call-form). */
template <typename TContext, typename TFrom, typename TEvent, typename TTo, typename = void>
struct is_factory_invocable : std::false_type {};
template <typename TContext, typename TFrom, typename TEvent, typename TTo>
struct is_factory_invocable <
TContext, TFrom, TEvent, TTo,
std::void_t<decltype (state_factory<TContext, TFrom, TEvent, TTo>::make (
std::declval<TContext &>(),
std::declval<const TFrom &>(),
std::declval<const TEvent &>())) >>
: std::bool_constant<std::is_same_v<
decltype (state_factory<TContext, TFrom, TEvent, TTo>::make (
std::declval<TContext &>(),
std::declval<const TFrom &>(),
std::declval<const TEvent &>())),
TTo>> {};
/**
* @brief FSM with runtime state storage and explicit compile-time validation.
*
* dispatch():
* - never static_asserts on missing transitions (avoids State×Event explosion with std::visit)
* - handles missing transitions via missing_transition<State,Event>::policy (ignore/strict)
*
* validate_events<Ev...>():
* - compile-time checks ONLY for pairs marked strict.
*/
template <typename TContext, typename TLogger, typename... TStates>
class fsm {
public:
using state_variant = std::variant<TStates...>;
template <typename TInitial,
typename = std::enable_if_t<is_one_of<TInitial, TStates...>::value>>
explicit fsm (TContext &ctx, TInitial initial, TLogger logger = TLogger{})
: m_ctx (ctx)
, m_logger (std::move (logger))
, m_state (std::move (initial)) {
std::visit (init_entry_visitor{*this}, m_state);
}
template <typename TEvent>
bool dispatch (const TEvent &ev) {
dispatch_visitor<TEvent> v{*this, ev};
return std::visit (v, m_state);
}
template <typename... TEvents>
static constexpr void validate_events() {
(validate_one_event<TEvents>(), ...);
}
const state_variant &state() const noexcept {
return m_state;
}
state_variant &state() noexcept {
return m_state;
}
private:
template <typename TEvent>
struct dispatch_visitor {
fsm &self;
const TEvent &ev;
template <typename TFrom>
bool operator() (TFrom &from) const {
return self.template dispatch_from<TFrom> (from, ev);
}
};
struct init_entry_visitor {
fsm &self;
template <typename TState>
void operator() (TState &st) const {
on_entry_state<TState>::call (self.m_ctx, st);
}
};
template <typename TEvent>
struct entry_after_commit_visitor {
fsm &self;
const TEvent &ev;
template <typename TState>
void operator() (TState &st) const {
on_entry_state<TState>::call (self.m_ctx, st);
on_entry_event<TState, TEvent>::call (self.m_ctx, st, ev);
}
};
template <typename TFrom, typename TEvent>
bool dispatch_from (TFrom &from, const TEvent &ev) {
using tr = typename transition<TFrom, TEvent>::type;
if constexpr (std::is_same_v<tr, no_transition>) {
if constexpr (missing_transition<TFrom, TEvent>::policy == missing_transition_policy::strict)
m_logger.unhandled_transition (name_of<TFrom>::value, name_of<TEvent>::value);
else
m_logger.ignored_transition (name_of<TFrom>::value, name_of<TEvent>::value);
return false;
} else {
using to_state = typename tr::to_state;
using guard_t = typename tr::guard;
using action_t = typename tr::action;
static_assert (is_one_of<to_state, TStates...>::value,
"FSM: transition target state not in FSM state list.");
static_assert (std::is_invocable_r_v<bool, guard_t, TContext &, const TFrom &, const TEvent &>,
"FSM: guard must be callable as bool(Context&, const From&, const Event&).");
static_assert (std::is_invocable_r_v<void, action_t, TContext &, const TFrom &, const to_state &, const TEvent &>,
"FSM: action must be callable as void(Context&, const From&, const To&, const Event&).");
static_assert (is_factory_invocable<TContext, TFrom, TEvent, to_state>::value,
"FSM: state_factory<Context,From,Event,To>::make must be callable as "
"To make(Context&, const From&, const Event&).");
guard_t guard{};
if (!guard (m_ctx, from, ev)) {
m_logger.guard_blocked (name_of<TFrom>::value, name_of<TEvent>::value);
return false;
}
on_exit_event<TFrom, TEvent>::call (m_ctx, from, ev);
on_exit_state<TFrom>::call (m_ctx, from);
to_state to = state_factory<TContext, TFrom, TEvent, to_state>::make (m_ctx, from, ev);
action_t action{};
action (m_ctx, from, to, ev);
m_state = std::move (to);
std::visit (entry_after_commit_visitor<TEvent> {*this, ev}, m_state);
return true;
}
}
template <typename TEvent>
static constexpr void validate_one_event() {
(validate_pair<TStates, TEvent>(), ...);
}
template <typename TState, typename TEvent>
static constexpr void validate_pair() {
if constexpr (missing_transition<TState, TEvent>::policy == missing_transition_policy::strict) {
static_assert (has_transition<TState, TEvent>::value,
"FSM validation: required (strict) transition is missing for (State, Event).");
}
}
private:
TContext &m_ctx;
TLogger m_logger;
state_variant m_state;
};
-10
View File
@@ -1,10 +0,0 @@
TEMPLATE = app
CONFIG += console c++17
CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
main.cpp
HEADERS += \
fsm.h
-372
View File
@@ -1,372 +0,0 @@
#include "fsm.h"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <getopt.h>
#include <iostream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
// ------------------------------------------------------------
// Data model
// ------------------------------------------------------------
struct Quote {
std::string meta;
std::vector<std::string> text_lines;
};
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) {
// Conservative escaping for LaTeX special chars.
std::string out;
out.reserve (s.size() + s.size() / 8);
for (const 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;
}
// ------------------------------------------------------------
// FSM: parsing Kindle "My Clippings" style format
// ------------------------------------------------------------
struct StExpectTitle {};
struct StExpectMeta {};
struct StExpectBlank {};
struct StCollectText {};
struct EvTitle {
std::string s;
};
struct EvMeta {
std::string s;
};
struct EvBlank {};
struct EvText {
std::string s;
};
struct EvSep {};
struct EvEof {};
struct ParserContext {
// group storage
std::vector<std::string> order;
std::unordered_map<std::string, std::vector<Quote>> by_title;
std::unordered_map<std::string, std::size_t> seen;
// currently parsed quote
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_if_ready() {
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();
}
};
struct SilentLogger : null_logger {};
// Actions
struct ActSetTitle {
void operator() (ParserContext &ctx, const StExpectTitle &, const StExpectMeta &, const EvTitle &ev) const {
ctx.start_title (ev.s);
}
};
struct ActSetMeta {
void operator() (ParserContext &ctx, const StExpectMeta &, const StExpectBlank &, const EvMeta &ev) const {
ctx.set_meta (ev.s);
}
};
struct ActAddText {
template <typename TFrom>
void operator() (ParserContext &ctx, const TFrom &, const StCollectText &, const EvText &ev) const {
ctx.add_text (ev.s);
}
};
struct ActFinalizeOnSep {
template <typename TFrom>
void operator() (ParserContext &ctx, const TFrom &, const StExpectTitle &, const EvSep &) const {
ctx.finalize_quote_if_ready();
}
};
struct ActFinalizeOnEof {
template <typename TFrom>
void operator() (ParserContext &ctx, const TFrom &, const StExpectTitle &, const EvEof &) const {
ctx.finalize_quote_if_ready();
}
};
// Transitions
template <> struct transition<StExpectTitle, EvTitle> {
using type = transition_to<StExpectMeta, always_allow, ActSetTitle>;
};
template <> struct transition<StExpectTitle, EvBlank> {
using type = transition_to<StExpectTitle>;
};
template <> struct transition<StExpectTitle, EvEof> {
using type = transition_to<StExpectTitle, always_allow, ActFinalizeOnEof>;
};
template <> struct transition<StExpectMeta, EvMeta> {
using type = transition_to<StExpectBlank, always_allow, ActSetMeta>;
};
template <> struct transition<StExpectBlank, EvBlank> {
using type = transition_to<StCollectText>;
};
template <> struct transition<StExpectBlank, EvText> {
using type = transition_to<StCollectText, always_allow, ActAddText>;
};
template <> struct transition<StCollectText, EvText> {
using type = transition_to<StCollectText, always_allow, ActAddText>;
};
template <> struct transition<StCollectText, EvSep> {
using type = transition_to<StExpectTitle, always_allow, ActFinalizeOnSep>;
};
template <> struct transition<StCollectText, EvEof> {
using type = transition_to<StExpectTitle, always_allow, ActFinalizeOnEof>;
};
// Strict contract for the pairs we dispatch.
template <> struct missing_transition<StExpectTitle, EvTitle> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StExpectTitle, EvBlank> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StExpectTitle, EvEof> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StExpectMeta, EvMeta> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StExpectBlank, EvBlank> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StExpectBlank, EvText> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StCollectText, EvText> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StCollectText, EvSep> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
template <> struct missing_transition<StCollectText, EvEof> {
static constexpr missing_transition_policy policy = missing_transition_policy::strict;
};
using ParserFsm = fsm<ParserContext, SilentLogger, StExpectTitle, StExpectMeta, StExpectBlank, StCollectText>;
static constexpr void validate_fsm_contract() {
ParserFsm::validate_events<EvTitle, EvMeta, EvBlank, EvText, EvSep, EvEof>();
}
// ------------------------------------------------------------
// CLI
// ------------------------------------------------------------
struct CliArgs {
std::string input;
std::string output;
};
static void print_usage (const char *argv0) {
std::cerr << "Usage: " << argv0 << " --input <file> --output <file>\n";
}
static bool parse_args (int argc, char **argv, CliArgs &out) {
static option long_opts[] = {
{"input", required_argument, nullptr, 'i'},
{"output", required_argument, nullptr, 'o'},
{"help", no_argument, nullptr, 'h'},
{nullptr, 0, nullptr, 0 }
};
int c = 0;
while ((c = ::getopt_long (argc, argv, "i:o:h", long_opts, nullptr)) != -1) {
switch (c) {
case 'i':
out.input = optarg;
break;
case 'o':
out.output = optarg;
break;
case 'h':
print_usage (argv[0]);
return false;
default:
print_usage (argv[0]);
return false;
}
}
if (out.input.empty() || out.output.empty()) {
print_usage (argv[0]);
return false;
}
return true;
}
// ------------------------------------------------------------
// Conversion
// ------------------------------------------------------------
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;
}
ParserContext ctx{};
ParserFsm fsm (ctx, StExpectTitle{}, SilentLogger{});
std::string line;
while (std::getline (in, line)) {
line = rstrip_cr (std::move (line));
if (line == "==========") {
fsm.dispatch (EvSep{});
continue;
}
const bool is_blank = line.empty();
const auto &st = fsm.state();
if (std::holds_alternative<StExpectTitle> (st)) {
if (is_blank)
fsm.dispatch (EvBlank{});
else
fsm.dispatch (EvTitle{line});
} else if (std::holds_alternative<StExpectMeta> (st)) {
// Kindle format expects meta here. If missing, keep going.
fsm.dispatch (EvMeta{line});
} else if (std::holds_alternative<StExpectBlank> (st)) {
if (is_blank)
fsm.dispatch (EvBlank{});
else
fsm.dispatch (EvText{line}); // some clippings have no blank line
} else {
// StCollectText
fsm.dispatch (EvText{line});
}
}
fsm.dispatch (EvEof{});
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;
}
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) {
validate_fsm_contract();
CliArgs args;
if (!parse_args (argc, argv, args))
return 1;
return convert (args.input, args.output);
}
+46
View File
@@ -0,0 +1,46 @@
#include "convertasciidoc.h"
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
bool ConverterAsciiDoc::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";
set_error_code (3);
return false;
}
const auto &book_titles = titles();
for (const auto &title : book_titles) {
output << "== " << title << "\n\n";
for (std::size_t quote_index = 0;
quote_index < quote_count (title);
++quote_index) {
output << "=== " << quote_meta (title, quote_index) << "\n\n";
output << "[quote]\n____\n";
const auto &text_lines = quote_text_lines (title, quote_index);
std::size_t line_index = 0;
if (!text_lines.empty() && text_lines.front().empty())
line_index = 1;
for (; line_index < text_lines.size(); ++line_index)
output << text_lines[line_index] << '\n';
output << "____\n\n";
output << "==== Notes\n\n";
}
}
set_error_code (0);
return true;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef CONVERTASCIIDOC_H
#define CONVERTASCIIDOC_H
#include "converter.h"
class ConverterAsciiDoc final : public Converter {
public:
using Converter::Converter;
bool save (const std::string &output_file_name) override;
};
#endif // CONVERTASCIIDOC_H
+46
View File
@@ -0,0 +1,46 @@
#include "convertorg.h"
#include <cerrno>
#include <cstring>
#include <fstream>
#include <iostream>
bool ConverterOrg::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";
set_error_code (3);
return false;
}
const auto &book_titles = titles();
for (const auto &title : book_titles) {
output << "* " << title << "\n\n";
for (std::size_t quote_index = 0;
quote_index < quote_count (title);
++quote_index) {
output << "** " << quote_meta (title, quote_index) << "\n\n";
output << "#+begin_quote\n";
const auto &text_lines = quote_text_lines (title, quote_index);
std::size_t line_index = 0;
if (!text_lines.empty() && text_lines.front().empty())
line_index = 1;
for (; line_index < text_lines.size(); ++line_index)
output << text_lines[line_index] << '\n';
output << "#+end_quote\n\n";
output << "*** Notes\n\n";
}
}
set_error_code (0);
return true;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef CONVERTORG_H
#define CONVERTORG_H
#include "converter.h"
class ConverterOrg final : public Converter {
public:
using Converter::Converter;
bool save (const std::string &output_file_name) override;
};
#endif // CONVERTORG_H
+4
View File
@@ -5,13 +5,17 @@ CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
convertasciidoc.cpp \
convertlatex.cpp \
convertmarkdown.cpp \
convertorg.cpp \
converter.cpp \
main.cpp
HEADERS += \
convertasciidoc.h \
convertlatex.h \
convertmarkdown.h \
convertorg.h \
converter.h \
typefactory.h
+25 -5
View File
@@ -3,25 +3,33 @@
#include <iostream>
#include <string>
#include "convertasciidoc.h"
#include "convertlatex.h"
#include "convertmarkdown.h"
#include "convertorg.h"
#include "typefactory.h"
#ifndef KINDLE2NOTES_VERSION
#define KINDLE2NOTES_VERSION "unknown"
#endif
using ConverterFactory = TypeFactory<std::string, Converter, std::string>;
static constexpr std::array<const char *, 2> kSupportedTypes = {
"latex", "md"
static constexpr std::array<const char *, 4> kSupportedTypes = {
"latex", "md", "org", "adoc"
};
struct CliArgs {
std::string input;
std::string output;
std::string type;
bool version = false;
};
static void print_usage (const char *argv0) {
std::cerr << "Usage: " << argv0
<< " --input <file> --output <file> --type <type>\n";
<< " --input <file> --output <file> --type <type>\n"
<< " " << argv0 << " --version\n";
}
static void print_supported_types() {
@@ -33,8 +41,10 @@ static void print_supported_types() {
static ConverterFactory build_converter_factory() {
ConverterFactory factory;
factory.registerType<ConverterAsciiDoc> ("adoc");
factory.registerType<ConverterLatex> ("latex");
factory.registerType<ConverterMarkdown> ("md");
factory.registerType<ConverterOrg> ("org");
return factory;
}
@@ -43,12 +53,13 @@ static bool parse_args (int argc, char **argv, CliArgs &out) {
{"input", required_argument, nullptr, 'i'},
{"output", required_argument, nullptr, 'o'},
{"type", required_argument, nullptr, 't'},
{"version", no_argument, nullptr, 'v'},
{"help", no_argument, nullptr, 'h'},
{nullptr, 0, nullptr, 0 }
};
int c = 0;
while ((c = ::getopt_long (argc, argv, "i:o:t:h", long_opts, nullptr)) != -1) {
while ((c = ::getopt_long (argc, argv, "i:o:t:vh", long_opts, nullptr)) != -1) {
switch (c) {
case 'i':
out.input = optarg;
@@ -59,6 +70,9 @@ static bool parse_args (int argc, char **argv, CliArgs &out) {
case 't':
out.type = optarg;
break;
case 'v':
out.version = true;
break;
case 'h':
print_usage (argv[0]);
return false;
@@ -68,7 +82,8 @@ static bool parse_args (int argc, char **argv, CliArgs &out) {
}
}
if (out.input.empty() || out.output.empty() || out.type.empty()) {
if (!out.version &&
(out.input.empty() || out.output.empty() || out.type.empty())) {
print_usage (argv[0]);
return false;
}
@@ -81,6 +96,11 @@ int main (int argc, char **argv) {
if (!parse_args (argc, argv, args))
return 1;
if (args.version) {
std::cout << "kindle2notes " << KINDLE2NOTES_VERSION << '\n';
return 0;
}
const ConverterFactory factory = build_converter_factory();
if (!factory.contains (args.type)) {
std::cerr << "Unsupported output type: " << args.type << '\n';
+4
View File
@@ -5,13 +5,17 @@ CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
convertasciidoc.cpp \
convertlatex.cpp \
convertmarkdown.cpp \
convertorg.cpp \
converter.cpp \
main.cpp
HEADERS += \
convertasciidoc.h \
convertlatex.h \
convertmarkdown.h \
convertorg.h \
converter.h \
typefactory.h