Compare commits
5
Commits
codex-test
...
1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be855ccebe | ||
|
|
6ed48022ae | ||
|
|
7026e246bb | ||
|
|
804876656d | ||
|
|
e5cb11c659 |
No files matched your search
+1
-1
@@ -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
@@ -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}
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
/files
|
||||||
|
/*.substvars
|
||||||
|
/kindle2notes/
|
||||||
Vendored
+5
@@ -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
|
||||||
Vendored
+16
@@ -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.
|
||||||
Vendored
+40
@@ -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.
|
||||||
+44
@@ -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:
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
3.0 (native)
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
tar-ignore = .git
|
||||||
|
tar-ignore = *.pro.user
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
TEMPLATE = app
|
|
||||||
CONFIG += console c++17
|
|
||||||
CONFIG -= app_bundle
|
|
||||||
CONFIG -= qt
|
|
||||||
|
|
||||||
SOURCES += \
|
|
||||||
main.cpp
|
|
||||||
|
|
||||||
HEADERS += \
|
|
||||||
fsm.h
|
|
||||||
-372
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user