5 Commits
Author SHA1 Message Date
deeaitch be855ccebe Add Debian package build 2026-08-28 01:46:43 -04:00
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
20 changed files with 390 additions and 685 deletions

No files matched your search

+1 -1
View File
@@ -24,6 +24,7 @@ tags
.directory .directory
*.debug *.debug
Makefile* Makefile*
/build-debian/
*.prl *.prl
*.app *.app
moc_*.cpp moc_*.cpp
@@ -71,4 +72,3 @@ CMakeLists.txt.user*
# -------- # --------
*.dll *.dll
*.exe *.exe
+39 -1
View File
@@ -1,11 +1,37 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(kindle2notes LANGUAGES CXX) project(kindle2notes VERSION 1.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(KINDLE2NOTES_VERSION "" CACHE STRING "Override the version reported by kindle2notes")
if(KINDLE2NOTES_VERSION STREQUAL "")
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()
endif()
message(STATUS "kindle2notes version: ${KINDLE2NOTES_VERSION}")
function(kindle2notes_enable_warnings target) function(kindle2notes_enable_warnings target)
if(MSVC) if(MSVC)
target_compile_options(${target} PRIVATE /W4) target_compile_options(${target} PRIVATE /W4)
@@ -15,13 +41,25 @@ function(kindle2notes_enable_warnings target)
endfunction() endfunction()
add_executable(kindle2notes add_executable(kindle2notes
typefactory/convertasciidoc.cpp
typefactory/convertasciidoc.h
typefactory/convertlatex.cpp typefactory/convertlatex.cpp
typefactory/convertlatex.h typefactory/convertlatex.h
typefactory/convertmarkdown.cpp typefactory/convertmarkdown.cpp
typefactory/convertmarkdown.h typefactory/convertmarkdown.h
typefactory/convertorg.cpp
typefactory/convertorg.h
typefactory/converter.cpp typefactory/converter.cpp
typefactory/converter.h typefactory/converter.h
typefactory/main.cpp typefactory/main.cpp
typefactory/typefactory.h typefactory/typefactory.h
) )
kindle2notes_enable_warnings(kindle2notes) kindle2notes_enable_warnings(kindle2notes)
target_compile_definitions(kindle2notes PRIVATE
KINDLE2NOTES_VERSION="${KINDLE2NOTES_VERSION}"
)
include(GNUInstallDirs)
install(TARGETS kindle2notes
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
+88 -1
View File
@@ -7,6 +7,8 @@ Supported output formats:
- LaTeX (`--type=latex`) - LaTeX (`--type=latex`)
- Markdown (`--type=md`) - Markdown (`--type=md`)
- Org mode (`--type=org`)
- AsciiDoc (`--type=adoc`)
Highlights are grouped by book title while preserving the order in which Highlights are grouped by book title while preserving the order in which
books first appear in the input file. 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 Empty lines inside highlights are preserved as `>` lines, and Markdown syntax
characters are escaped automatically. 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 ## Architecture
Input parsing is implemented as a stage-driven pipeline with one handler for Input parsing is implemented as a stage-driven pipeline with one handler for
@@ -92,6 +131,29 @@ The resulting executable is:
build/kindle2notes build/kindle2notes
``` ```
### Debian package
On Debian or a Debian-based distribution, install the package build tools and
build the binary package with:
```bash
sudo apt install build-essential cmake dpkg-dev
dpkg-buildpackage --build=binary --no-sign
```
The `.deb`, `.changes`, and `.buildinfo` files are written to the parent
directory. To create a Debian source package instead, run:
```bash
dpkg-buildpackage --build=source --no-sign
```
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 ## Usage
Generate LaTeX: Generate LaTeX:
@@ -112,17 +174,42 @@ Generate Markdown:
--type=md --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: Short options are also available:
```bash ```bash
./build/kindle2notes -i input.txt -o notes.md -t md ./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 | | Argument | Short form | Description |
| --- | --- | --- | | --- | --- | --- |
| `--input` | `-i` | Path to the Kindle clippings file | | `--input` | `-i` | Path to the Kindle clippings file |
| `--output` | `-o` | Path to the generated 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 | | `--help` | `-h` | Show usage information |
If an unsupported output type is requested, the program prints the list of If an unsupported output type is requested, the program prints the list of
+3
View File
@@ -0,0 +1,3 @@
/files
/*.substvars
/kindle2notes/
+5
View File
@@ -0,0 +1,5 @@
kindle2notes (1.1) unstable; urgency=medium
* Initial Debian package.
-- deeaitch <himrodim@gmail.com> Fri, 28 Aug 2026 01:46:16 -0400
+16
View File
@@ -0,0 +1,16 @@
Source: kindle2notes
Section: utils
Priority: optional
Maintainer: deeaitch <himrodim@gmail.com>
Build-Depends: cmake (>= 3.16), g++, dpkg-dev
Standards-Version: 4.6.2
Rules-Requires-Root: no
Homepage: https://git.himro.ca/deeaitch/kindle2notes
Package: kindle2notes
Architecture: any
Depends: ${shlibs:Depends}
Description: convert Kindle clippings into structured notes
kindle2notes converts Amazon Kindle My Clippings text exports into
human-editable LaTeX, Markdown, Org mode, or AsciiDoc documents. Highlights
are grouped by book title and include dedicated sections for personal notes.
+40
View File
@@ -0,0 +1,40 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: kindle2notes
Source: https://git.himro.ca/deeaitch/kindle2notes
Files: *
Copyright: 2026 deeaitch <himrodim@gmail.com>
License: GPL-3+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
On Debian systems, the complete text of the GNU General Public License
version 3 can be found in "/usr/share/common-licenses/GPL-3".
Files: typefactory/typefactory.h
Copyright: 2026 Dim Himro
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Vendored Executable
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/make -f
include /usr/share/dpkg/architecture.mk
include /usr/share/dpkg/buildflags.mk
PACKAGE := kindle2notes
BUILD_DIR := build-debian
PACKAGE_DIR := $(CURDIR)/debian/$(PACKAGE)
PACKAGE_VERSION := $(shell dpkg-parsechangelog -S Version)
.PHONY: clean build build-arch build-indep binary binary-arch binary-indep
clean:
rm -rf $(BUILD_DIR) $(PACKAGE_DIR)
rm -f debian/files debian/*.substvars
build: build-arch build-indep
build-arch:
cmake -S . -B $(BUILD_DIR) \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr \
-DKINDLE2NOTES_VERSION=$(PACKAGE_VERSION) \
-DCMAKE_CXX_FLAGS="$(CXXFLAGS) $(CPPFLAGS)" \
-DCMAKE_EXE_LINKER_FLAGS="$(LDFLAGS)"
cmake --build $(BUILD_DIR) --parallel
build-indep:
binary: binary-arch binary-indep
binary-arch: build-arch
DESTDIR=$(PACKAGE_DIR) cmake --install $(BUILD_DIR)
strip --strip-unneeded $(PACKAGE_DIR)/usr/bin/$(PACKAGE)
install -d $(PACKAGE_DIR)/usr/share/doc/$(PACKAGE)
install -m 0644 debian/copyright $(PACKAGE_DIR)/usr/share/doc/$(PACKAGE)/copyright
install -m 0644 README.md $(PACKAGE_DIR)/usr/share/doc/$(PACKAGE)/README.md
gzip -n -9 -c debian/changelog > $(PACKAGE_DIR)/usr/share/doc/$(PACKAGE)/changelog.Debian.gz
install -d $(PACKAGE_DIR)/DEBIAN
dpkg-shlibdeps -Tdebian/$(PACKAGE).substvars debian/$(PACKAGE)/usr/bin/$(PACKAGE)
dpkg-gencontrol -p$(PACKAGE) -P$(PACKAGE_DIR) -Tdebian/$(PACKAGE).substvars
dpkg-deb --build --root-owner-group $(PACKAGE_DIR) ..
binary-indep:
+1
View File
@@ -0,0 +1 @@
3.0 (native)
+2
View File
@@ -0,0 +1,2 @@
tar-ignore = .git
tar-ignore = *.pro.user
-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 CONFIG -= qt
SOURCES += \ SOURCES += \
convertasciidoc.cpp \
convertlatex.cpp \ convertlatex.cpp \
convertmarkdown.cpp \ convertmarkdown.cpp \
convertorg.cpp \
converter.cpp \ converter.cpp \
main.cpp main.cpp
HEADERS += \ HEADERS += \
convertasciidoc.h \
convertlatex.h \ convertlatex.h \
convertmarkdown.h \ convertmarkdown.h \
convertorg.h \
converter.h \ converter.h \
typefactory.h typefactory.h
+25 -5
View File
@@ -3,25 +3,33 @@
#include <iostream> #include <iostream>
#include <string> #include <string>
#include "convertasciidoc.h"
#include "convertlatex.h" #include "convertlatex.h"
#include "convertmarkdown.h" #include "convertmarkdown.h"
#include "convertorg.h"
#include "typefactory.h" #include "typefactory.h"
#ifndef KINDLE2NOTES_VERSION
#define KINDLE2NOTES_VERSION "unknown"
#endif
using ConverterFactory = TypeFactory<std::string, Converter, std::string>; using ConverterFactory = TypeFactory<std::string, Converter, std::string>;
static constexpr std::array<const char *, 2> kSupportedTypes = { static constexpr std::array<const char *, 4> kSupportedTypes = {
"latex", "md" "latex", "md", "org", "adoc"
}; };
struct CliArgs { struct CliArgs {
std::string input; std::string input;
std::string output; std::string output;
std::string type; std::string type;
bool version = false;
}; };
static void print_usage (const char *argv0) { static void print_usage (const char *argv0) {
std::cerr << "Usage: " << 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() { static void print_supported_types() {
@@ -33,8 +41,10 @@ static void print_supported_types() {
static ConverterFactory build_converter_factory() { static ConverterFactory build_converter_factory() {
ConverterFactory factory; ConverterFactory factory;
factory.registerType<ConverterAsciiDoc> ("adoc");
factory.registerType<ConverterLatex> ("latex"); factory.registerType<ConverterLatex> ("latex");
factory.registerType<ConverterMarkdown> ("md"); factory.registerType<ConverterMarkdown> ("md");
factory.registerType<ConverterOrg> ("org");
return factory; return factory;
} }
@@ -43,12 +53,13 @@ static bool parse_args (int argc, char **argv, CliArgs &out) {
{"input", required_argument, nullptr, 'i'}, {"input", required_argument, nullptr, 'i'},
{"output", required_argument, nullptr, 'o'}, {"output", required_argument, nullptr, 'o'},
{"type", required_argument, nullptr, 't'}, {"type", required_argument, nullptr, 't'},
{"version", no_argument, nullptr, 'v'},
{"help", no_argument, nullptr, 'h'}, {"help", no_argument, nullptr, 'h'},
{nullptr, 0, nullptr, 0 } {nullptr, 0, nullptr, 0 }
}; };
int c = 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) { switch (c) {
case 'i': case 'i':
out.input = optarg; out.input = optarg;
@@ -59,6 +70,9 @@ static bool parse_args (int argc, char **argv, CliArgs &out) {
case 't': case 't':
out.type = optarg; out.type = optarg;
break; break;
case 'v':
out.version = true;
break;
case 'h': case 'h':
print_usage (argv[0]); print_usage (argv[0]);
return false; 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]); print_usage (argv[0]);
return false; return false;
} }
@@ -81,6 +96,11 @@ int main (int argc, char **argv) {
if (!parse_args (argc, argv, args)) if (!parse_args (argc, argv, args))
return 1; return 1;
if (args.version) {
std::cout << "kindle2notes " << KINDLE2NOTES_VERSION << '\n';
return 0;
}
const ConverterFactory factory = build_converter_factory(); const ConverterFactory factory = build_converter_factory();
if (!factory.contains (args.type)) { if (!factory.contains (args.type)) {
std::cerr << "Unsupported output type: " << args.type << '\n'; std::cerr << "Unsupported output type: " << args.type << '\n';
+4
View File
@@ -5,13 +5,17 @@ CONFIG -= app_bundle
CONFIG -= qt CONFIG -= qt
SOURCES += \ SOURCES += \
convertasciidoc.cpp \
convertlatex.cpp \ convertlatex.cpp \
convertmarkdown.cpp \ convertmarkdown.cpp \
convertorg.cpp \
converter.cpp \ converter.cpp \
main.cpp main.cpp
HEADERS += \ HEADERS += \
convertasciidoc.h \
convertlatex.h \ convertlatex.h \
convertmarkdown.h \ convertmarkdown.h \
convertorg.h \
converter.h \ converter.h \
typefactory.h typefactory.h