fyodor 0.2.1 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +15 -7
- data/bin/fyodor +55 -11
- data/lib/fyodor/clippings_parser.rb +1 -4
- data/lib/fyodor/entry.rb +35 -21
- data/lib/fyodor/entry_parser.rb +56 -42
- data/lib/fyodor/md_generator.rb +6 -12
- data/lib/fyodor/version.rb +1 -1
- metadata +3 -4
- data/lib/fyodor/cli.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dae694865dbdb0187906ad21b610ac7f9486819141f86392816a41fbe835f71c
|
4
|
+
data.tar.gz: 97c07e5ba51877015426221aed2158a20436f36d8afbca165b8433c20050e02a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec0b9492639f2b77ba2523106db931b3363c42731cab3b456f83e6c7e91ccc6ec196d4cbe9d8798f62a5c452eed391fec521d7e8da56c537be51a224e93963bc
|
7
|
+
data.tar.gz: 359197fe0aeb85d76f1d025c2e3517a7fb02c60712989f79dd14be2004f50f3892a312b4114a3d29245d26a8f2e72f59dd4a2925c7c800f3dc63b435d63e4952
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ Convert your Amazon Kindle highlights, notes and bookmarks into markdown files.
|
|
6
6
|
|
7
7
|
This application parses `My Clippings.txt` from your Kindle and generates a markdown file for each book/document, in the format `#{Author} - #{Title}.md`. This way, your annotations are conveniently stored and easily managed.
|
8
8
|
|
9
|
-
[For samples of the output, click here.](
|
9
|
+
[For samples of the output, click here.](docs/output_demo)
|
10
10
|
|
11
11
|
To read more about the motivation and what problem it tries to solve, [check this blog post](http://rafaelc.org/blog/export-all-your-kindle-highlights-and-notes/).
|
12
12
|
|
@@ -30,18 +30,26 @@ Install Ruby and run:
|
|
30
30
|
$ gem install fyodor
|
31
31
|
```
|
32
32
|
|
33
|
+
## Updating
|
34
|
+
|
35
|
+
Run:
|
36
|
+
|
37
|
+
```
|
38
|
+
$ gem update fyodor
|
39
|
+
```
|
40
|
+
|
33
41
|
## Configuration
|
34
42
|
|
35
|
-
Fyodor has an optional configuration file, which is used for the following
|
43
|
+
Fyodor has an optional configuration file, which is used for the following.
|
36
44
|
|
37
45
|
### Languages
|
38
46
|
|
39
|
-
If your Kindle is not in English, you should tell Fyodor how some things are called by your `My Clippings.txt` (e.g. highlights, pages, etc).
|
47
|
+
If your Kindle is not in English, you should tell Fyodor how some things are called by your `My Clippings.txt` (e.g. highlights, pages, etc). _Fyodor should still work without configuration, but you won't take advantage of many features, resulting in a dirtier output._
|
40
48
|
|
41
49
|
1. Download the sample config to `~/.config/fyodor.toml` or `$XDG_CONFIG_HOME/fyodor.toml`:
|
42
50
|
|
43
51
|
```
|
44
|
-
$ wget https://raw.githubusercontent.com/rccavalcanti/fyodor/master/fyodor.toml.sample -O ~/.config/fyodor.toml
|
52
|
+
$ wget https://raw.githubusercontent.com/rccavalcanti/fyodor/master/docs/fyodor.toml.sample -O ~/.config/fyodor.toml
|
45
53
|
```
|
46
54
|
|
47
55
|
2. Open both the configuration and your `My Clippings.txt` in your preferred editor. Change the values in the `[parser]` section to mirror what you get in `My Clippings.txt`.
|
@@ -74,8 +82,8 @@ Where:
|
|
74
82
|
- `CLIPPINGS_FILE` is the path for `My Clippings.txt`.
|
75
83
|
- `OUTPUT_DIR` is the directory to write the markdown files. If none is supplied, this will be `fyodor_output` under the current directory.
|
76
84
|
|
77
|
-
##
|
85
|
+
## License
|
78
86
|
|
79
|
-
|
87
|
+
Licensed under [GPLv3](LICENSE)
|
80
88
|
|
81
|
-
Copyright 2019 Rafael Cavalcanti
|
89
|
+
Copyright (C) 2019-2020 [Rafael Cavalcanti](https://rafaelc.org/)
|
data/bin/fyodor
CHANGED
@@ -1,15 +1,59 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require "fyodor
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
3
|
+
require "fyodor"
|
4
|
+
require "fyodor/config_getter"
|
5
|
+
require "fyodor/stats_printer"
|
6
|
+
require "fyodor/version"
|
7
|
+
require "optimist"
|
8
|
+
require "pathname"
|
9
|
+
|
10
|
+
module Fyodor
|
11
|
+
class CLI
|
12
|
+
def main
|
13
|
+
parse_args
|
14
|
+
@config = ConfigGetter.new.config
|
15
|
+
|
16
|
+
if @cli_opts.trace
|
17
|
+
run
|
18
|
+
else
|
19
|
+
begin
|
20
|
+
run
|
21
|
+
rescue StandardError => e
|
22
|
+
abort(e.message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def get_path(path)
|
30
|
+
Pathname.new(path).expand_path
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_output_dir
|
34
|
+
Pathname.new(Dir.pwd) + "fyodor_output"
|
35
|
+
end
|
36
|
+
|
37
|
+
def run
|
38
|
+
library = Library.new
|
39
|
+
ClippingsParser.new(@clippings_path, @config["parser"]).parse(library)
|
40
|
+
StatsPrinter.new(library).print
|
41
|
+
OutputWriter.new(library, @output_dir, @config["output"]).write_all
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_args
|
45
|
+
@cli_opts = Optimist::options do
|
46
|
+
version "Fyodor v#{Fyodor::VERSION}"
|
47
|
+
synopsis "Usage: fyodor [options] my_clippings_path [output_dir]"
|
48
|
+
|
49
|
+
opt :trace, "Show backtrace", :default => false
|
50
|
+
end
|
51
|
+
|
52
|
+
Optimist::die "Wrong number of arguments." if ARGV.count != 1 && ARGV.count != 2
|
53
|
+
@clippings_path = get_path(ARGV[0])
|
54
|
+
@output_dir = ARGV[1].nil? ? default_output_dir : get_path(ARGV[1])
|
55
|
+
end
|
14
56
|
end
|
15
57
|
end
|
58
|
+
|
59
|
+
Fyodor::CLI.new.main
|
@@ -3,7 +3,6 @@ require_relative "entry_parser"
|
|
3
3
|
module Fyodor
|
4
4
|
class ClippingsParser
|
5
5
|
SEPARATOR = /^==========\r?\n$/
|
6
|
-
ENTRY_SIZE = 5
|
7
6
|
|
8
7
|
def initialize(clippings_path, parser_config)
|
9
8
|
@path = clippings_path
|
@@ -25,9 +24,7 @@ module Fyodor
|
|
25
24
|
private
|
26
25
|
|
27
26
|
def end_entry?(lines)
|
28
|
-
|
29
|
-
return true if lines.size == ENTRY_SIZE && lines.last =~ SEPARATOR
|
30
|
-
raise "MyClippings is badly formatted"
|
27
|
+
lines.last =~ SEPARATOR
|
31
28
|
end
|
32
29
|
|
33
30
|
def parse_entry(lines)
|
data/lib/fyodor/entry.rb
CHANGED
@@ -1,23 +1,35 @@
|
|
1
1
|
module Fyodor
|
2
2
|
class Entry
|
3
|
-
TYPE = {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
3
|
+
TYPE = {
|
4
|
+
note: "note",
|
5
|
+
highlight: "highlight",
|
6
|
+
bookmark: "bookmark",
|
7
|
+
clip: "clip"
|
8
|
+
}
|
9
|
+
|
10
|
+
ATTRS = [
|
11
|
+
:book_title,
|
12
|
+
:book_author,
|
13
|
+
:text,
|
14
|
+
:desc,
|
15
|
+
:type,
|
16
|
+
:loc,
|
17
|
+
:loc_start,
|
18
|
+
:page,
|
19
|
+
:page_start,
|
20
|
+
:time
|
21
|
+
]
|
22
|
+
|
23
|
+
attr_reader *ATTRS
|
24
|
+
|
25
|
+
def initialize(args)
|
26
|
+
ATTRS.each do |attr|
|
27
|
+
instance_variable_set("@#{attr}", args[attr])
|
28
|
+
end
|
29
|
+
|
30
|
+
# These are our comparables. Let's make sure they are numbers.
|
31
|
+
@loc_start = @loc_start.to_i
|
32
|
+
@page_start = @page_start.to_i
|
21
33
|
|
22
34
|
raise ArgumentError, "Invalid Entry type" unless TYPE.value?(@type) || @type.nil?
|
23
35
|
end
|
@@ -31,11 +43,13 @@ module Fyodor
|
|
31
43
|
end
|
32
44
|
|
33
45
|
def desc_parsed?
|
34
|
-
@loc_start != 0
|
46
|
+
! @type.nil? && (@loc_start != 0 || @page_start != 0)
|
35
47
|
end
|
36
48
|
|
37
49
|
# Override this method to use a SortedSet.
|
38
50
|
def <=>(other)
|
51
|
+
return @page_start <=> other.page_start if @loc_start == 0
|
52
|
+
|
39
53
|
@loc_start <=> other.loc_start
|
40
54
|
end
|
41
55
|
|
@@ -44,7 +58,7 @@ module Fyodor
|
|
44
58
|
return false if @type != other.type || @text != other.text
|
45
59
|
|
46
60
|
if desc_parsed? && other.desc_parsed?
|
47
|
-
@loc == other.loc
|
61
|
+
@loc == other.loc && @page == other.page
|
48
62
|
else
|
49
63
|
@desc == other.desc
|
50
64
|
end
|
@@ -54,7 +68,7 @@ module Fyodor
|
|
54
68
|
|
55
69
|
def hash
|
56
70
|
if desc_parsed?
|
57
|
-
@text.hash ^ @type.hash ^ @loc.hash
|
71
|
+
@text.hash ^ @type.hash ^ @loc.hash ^ @page.hash
|
58
72
|
else
|
59
73
|
@text.hash ^ @desc.hash
|
60
74
|
end
|
data/lib/fyodor/entry_parser.rb
CHANGED
@@ -9,26 +9,37 @@ module Fyodor
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def entry
|
12
|
-
Entry.new({
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
12
|
+
Entry.new({
|
13
|
+
book_title: book[:title],
|
14
|
+
book_author: book[:author],
|
15
|
+
text: text,
|
16
|
+
desc: desc,
|
17
|
+
type: type,
|
18
|
+
loc: loc,
|
19
|
+
loc_start: loc_start,
|
20
|
+
page: page,
|
21
|
+
page_start: page_start,
|
22
|
+
time: time
|
23
|
+
})
|
21
24
|
end
|
22
25
|
|
23
26
|
|
24
27
|
private
|
25
28
|
|
26
29
|
def book
|
27
|
-
|
30
|
+
regex = /^(.*) \((.*)\)\r?\n$/
|
31
|
+
|
32
|
+
title, author = @lines[0].scan(regex).first
|
28
33
|
# If book has no author, regex fails.
|
29
|
-
|
34
|
+
if title.nil?
|
35
|
+
title = @lines[0]
|
36
|
+
author = ""
|
37
|
+
end
|
38
|
+
|
39
|
+
title = rm_zero_width_chars(title).strip
|
40
|
+
author = rm_zero_width_chars(author).strip
|
30
41
|
|
31
|
-
{title: title
|
42
|
+
{title: title, author: author}
|
32
43
|
end
|
33
44
|
|
34
45
|
def desc
|
@@ -36,58 +47,61 @@ module Fyodor
|
|
36
47
|
end
|
37
48
|
|
38
49
|
def type
|
39
|
-
Entry::TYPE.values.find
|
50
|
+
Entry::TYPE.values.find do |type|
|
51
|
+
keyword = Regexp.quote(@config[type])
|
52
|
+
regex = /^- #{keyword}/i
|
53
|
+
|
54
|
+
@lines[1] =~ regex
|
55
|
+
end
|
40
56
|
end
|
41
57
|
|
42
58
|
def loc
|
43
|
-
@
|
59
|
+
keyword = Regexp.quote(@config["loc"])
|
60
|
+
regex = /#{keyword} (\S+)/i
|
61
|
+
|
62
|
+
@lines[1][regex, 1]
|
44
63
|
end
|
45
64
|
|
46
65
|
def loc_start
|
47
|
-
@
|
66
|
+
keyword = Regexp.quote(@config["loc"])
|
67
|
+
regex = /#{keyword} (\d+)(-\d+)?/i
|
68
|
+
|
69
|
+
@lines[1][regex, 1].to_i
|
48
70
|
end
|
49
71
|
|
50
72
|
def page
|
51
|
-
@
|
52
|
-
|
73
|
+
keyword = Regexp.quote(@config["page"])
|
74
|
+
regex = /#{keyword} (\S+)/i
|
53
75
|
|
54
|
-
|
55
|
-
@lines[1][regex_cap(:time), 1]
|
76
|
+
@lines[1][regex, 1]
|
56
77
|
end
|
57
78
|
|
58
|
-
def
|
59
|
-
@
|
79
|
+
def page_start
|
80
|
+
keyword = Regexp.quote(@config["page"])
|
81
|
+
regex = /#{keyword} (\d+)(-\d+)?/i
|
82
|
+
|
83
|
+
@lines[1][regex, 1].to_i
|
60
84
|
end
|
61
85
|
|
62
|
-
def
|
63
|
-
|
64
|
-
|
86
|
+
def time
|
87
|
+
keyword = Regexp.quote(@config["time"])
|
88
|
+
regex = /#{keyword} (.*)\r?\n$/i
|
89
|
+
|
90
|
+
@lines[1][regex, 1]
|
65
91
|
end
|
66
92
|
|
67
|
-
def
|
68
|
-
|
69
|
-
when :title_author
|
70
|
-
/^(.*) \((.*)\)\r?\n$/
|
71
|
-
when :loc
|
72
|
-
s = Regexp.quote(@config["loc"])
|
73
|
-
/#{s} (\S+)/
|
74
|
-
when :loc_start
|
75
|
-
s = Regexp.quote(@config["loc"])
|
76
|
-
/#{s} (\d+)(-\d+)?/
|
77
|
-
when :page
|
78
|
-
s = Regexp.quote(@config["page"])
|
79
|
-
/#{s} (\S+)/
|
80
|
-
when :time
|
81
|
-
s = Regexp.quote(@config["time"])
|
82
|
-
/#{s} (.*)\r?\n$/
|
83
|
-
end
|
93
|
+
def text
|
94
|
+
@lines[3..-2].join.strip
|
84
95
|
end
|
85
96
|
|
86
97
|
def format_check
|
87
|
-
raise "Entry must have five lines" unless @lines.size == 5
|
88
98
|
raise "Entry is badly formatted" if @lines[0].strip.empty?
|
89
99
|
raise "Entry is badly formatted" if @lines[1].strip.empty?
|
90
100
|
raise "Entry is badly formatted" unless @lines[2].strip.empty?
|
91
101
|
end
|
102
|
+
|
103
|
+
def rm_zero_width_chars(str)
|
104
|
+
str.gsub(/[\u200B-\u200D\uFEFF]/, '')
|
105
|
+
end
|
92
106
|
end
|
93
107
|
end
|
data/lib/fyodor/md_generator.rb
CHANGED
@@ -52,25 +52,24 @@ module Fyodor
|
|
52
52
|
output = "---\n\n"
|
53
53
|
output += "## #{title}\n\n" unless title.nil?
|
54
54
|
entries.each do |entry|
|
55
|
-
output += "#{
|
56
|
-
output += "<p style=\"text-align: right;\"><sup>#{
|
55
|
+
output += "#{item_text(entry)}\n\n"
|
56
|
+
output += "<p style=\"text-align: right;\"><sup>#{item_desc(entry)}</sup></p>\n\n"
|
57
57
|
end
|
58
58
|
output
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
61
|
+
def item_text(entry)
|
62
62
|
case entry.type
|
63
63
|
when Entry::TYPE[:bookmark]
|
64
64
|
"* #{page(entry)}"
|
65
65
|
when Entry::TYPE[:note]
|
66
|
-
|
67
|
-
"> _#{text(entry)}_"
|
66
|
+
"* _Note:_\n#{entry.text.strip}"
|
68
67
|
else
|
69
|
-
"
|
68
|
+
"* #{entry.text.strip}"
|
70
69
|
end
|
71
70
|
end
|
72
71
|
|
73
|
-
def
|
72
|
+
def item_desc(entry)
|
74
73
|
return entry.desc unless entry.desc_parsed?
|
75
74
|
|
76
75
|
case entry.type
|
@@ -93,10 +92,5 @@ module Fyodor
|
|
93
92
|
def type(entry)
|
94
93
|
SINGULAR[entry.type]
|
95
94
|
end
|
96
|
-
|
97
|
-
def text(entry)
|
98
|
-
# Markdown needs no white space between text and formatters
|
99
|
-
entry.text.strip
|
100
|
-
end
|
101
95
|
end
|
102
96
|
end
|
data/lib/fyodor/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fyodor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rafael Cavalcanti
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: toml
|
@@ -36,7 +36,6 @@ files:
|
|
36
36
|
- bin/fyodor
|
37
37
|
- lib/fyodor.rb
|
38
38
|
- lib/fyodor/book.rb
|
39
|
-
- lib/fyodor/cli.rb
|
40
39
|
- lib/fyodor/clippings_parser.rb
|
41
40
|
- lib/fyodor/config_getter.rb
|
42
41
|
- lib/fyodor/core_extensions/hash/merging.rb
|
@@ -68,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
67
|
- !ruby/object:Gem::Version
|
69
68
|
version: '0'
|
70
69
|
requirements: []
|
71
|
-
rubygems_version: 3.
|
70
|
+
rubygems_version: 3.1.2
|
72
71
|
signing_key:
|
73
72
|
specification_version: 4
|
74
73
|
summary: Kindle clippings parser
|
data/lib/fyodor/cli.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
require "fyodor/config_getter"
|
2
|
-
require "fyodor/stats_printer"
|
3
|
-
require "fyodor/library"
|
4
|
-
require "fyodor/clippings_parser"
|
5
|
-
require "fyodor/output_writer"
|
6
|
-
require "pathname"
|
7
|
-
|
8
|
-
module Fyodor
|
9
|
-
class CLI
|
10
|
-
USAGE = "Usage: #{File.basename($0)} my_clippings_path [output_dir]"
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
get_args
|
14
|
-
@config = ConfigGetter.new.config
|
15
|
-
end
|
16
|
-
|
17
|
-
def main
|
18
|
-
library = Library.new
|
19
|
-
ClippingsParser.new(@clippings_path, @config["parser"]).parse(library)
|
20
|
-
StatsPrinter.new(library).print
|
21
|
-
OutputWriter.new(library, @output_dir, @config["output"]).write_all
|
22
|
-
end
|
23
|
-
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def get_args
|
28
|
-
abort(USAGE) if ARGV.count != 1 && ARGV.count != 2
|
29
|
-
|
30
|
-
@clippings_path = get_path(ARGV[0])
|
31
|
-
@output_dir = ARGV[1].nil? ? default_output_dir : get_path(ARGV[1])
|
32
|
-
end
|
33
|
-
|
34
|
-
def get_path(path)
|
35
|
-
Pathname.new(path).expand_path
|
36
|
-
end
|
37
|
-
|
38
|
-
def default_output_dir
|
39
|
-
Pathname.new(Dir.pwd) + "fyodor_output"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|