fyodor 0.2.1 → 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|