fyodor 0.2.10 → 0.3.3
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 +42 -18
- data/lib/fyodor/book.rb +3 -3
- data/lib/fyodor/config_getter.rb +33 -20
- data/lib/fyodor/entry.rb +5 -7
- data/lib/fyodor/output_generator.rb +72 -0
- data/lib/fyodor/output_writer.rb +6 -4
- data/lib/fyodor/version.rb +1 -1
- data/share/template.erb +16 -0
- metadata +7 -6
- data/lib/fyodor/md_generator.rb +0 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7fa1b1e78cc0ab15159e69a7f63c51ff94d549a618059395d29401ddc16ff45c
|
4
|
+
data.tar.gz: 325bfb536a54858e628a81094b707f7cd3f0be83d2cf613cf6b8b8de79b33015
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e5f8277ece0aa866131788f2702a1dc137cea14d86795e2c71c74947d9c532482b2bc0a89e34d7500af69a15642a774ce3a815882f835b4ce34cd155ec96af2
|
7
|
+
data.tar.gz: d8a7b83be1154a5385f8870e94f8973de8f8b744ffb09122d55dae9282b074ede788fba4ca1eccb9d1263786ad478c16d6d02a6bd1466e42aab603ed4fe22d53
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Fyodor
|
2
2
|
|
3
|
-
Convert your Amazon Kindle highlights, notes and bookmarks into markdown
|
3
|
+
Convert your Amazon Kindle highlights, notes and bookmarks into markdown (or any format).
|
4
4
|
|
5
5
|
## What is Fyodor
|
6
6
|
|
@@ -17,8 +17,9 @@ To read more about the motivation and what problem it tries to solve, [check thi
|
|
17
17
|
- Orders your entries by location/page on each book (the clippings file is ordered by time).
|
18
18
|
- Easily configurable for your language, allowing you to get all features and beautiful output.
|
19
19
|
- This software goes some length to be locale agnostic: basic parsing should work without configuration for any language. It should also work even if your clippings file has multiple locales.
|
20
|
-
-
|
21
|
-
-
|
20
|
+
- By default, output in a format that is clean and easy to edit/fiddle around: markdown.
|
21
|
+
- Bookmarks are placed together and notes are formatted differently, for better visualization.
|
22
|
+
- Entirely customizable output, in the format you prefer, through templates.
|
22
23
|
|
23
24
|
This program is based on the clippings file generated by Kindle 2019, but should work with other models.
|
24
25
|
|
@@ -33,7 +34,7 @@ We are limited by the data Kindle makes available through `My Clippings.txt`. Th
|
|
33
34
|
|
34
35
|
Install Ruby and run:
|
35
36
|
|
36
|
-
```
|
37
|
+
```shell-session
|
37
38
|
$ gem install fyodor
|
38
39
|
```
|
39
40
|
|
@@ -41,29 +42,29 @@ $ gem install fyodor
|
|
41
42
|
|
42
43
|
Run:
|
43
44
|
|
44
|
-
```
|
45
|
+
```shell-session
|
45
46
|
$ gem update fyodor
|
46
47
|
```
|
47
48
|
|
48
49
|
## Configuration
|
49
50
|
|
50
|
-
Fyodor
|
51
|
-
|
52
|
-
### Languages
|
53
|
-
|
54
|
-
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._
|
51
|
+
Fyodor reads an optional configuration file at `~/.config/fyodor/fyodor.toml` or `$XDG_CONFIG_HOME/fyodor/fyodor.toml`. This section describes the available parameters (none is required).
|
55
52
|
|
56
|
-
|
53
|
+
To download the [sample configuration](docs/fyodor.toml.sample):
|
57
54
|
|
58
|
-
```
|
55
|
+
```shell-session
|
59
56
|
$ curl https://raw.githubusercontent.com/rc2dev/fyodor/master/docs/fyodor.toml.sample --create-dirs -o ~/.config/fyodor/fyodor.toml
|
60
57
|
```
|
61
58
|
|
62
|
-
|
59
|
+
### Languages
|
60
|
+
|
61
|
+
If your Kindle device is not set to English (US), you should tell Fyodor how some things are named on your `My Clippings.txt` (highlights, pages, etc). I went through some length to make Fyodor work regardless of this step. However, if you don't set this correctly, you won't take advantage of many features, resulting in a dirtier output.
|
62
|
+
|
63
|
+
Open both `fyodor.toml` 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`.
|
63
64
|
|
64
65
|
For example, this is the configuration for Brazilian Portuguese:
|
65
66
|
|
66
|
-
```
|
67
|
+
```toml
|
67
68
|
[parser]
|
68
69
|
highlight = "Seu destaque"
|
69
70
|
note = "Sua nota"
|
@@ -76,18 +77,41 @@ time = "Adicionado:"
|
|
76
77
|
|
77
78
|
### Showing the time
|
78
79
|
|
79
|
-
|
80
|
+
You can also set whether to print the time of each entry. Under `[output]`, set `time` to `true` or `false`:
|
80
81
|
|
81
|
-
|
82
|
+
```toml
|
83
|
+
[output]
|
84
|
+
time = true
|
85
|
+
```
|
82
86
|
|
87
|
+
### Extension
|
88
|
+
|
89
|
+
If you want to change the extension of the output files (typically after changing the template), set `extension` under `[output]`. For example, to change it to HTML:
|
90
|
+
|
91
|
+
```toml
|
92
|
+
[output]
|
93
|
+
extension = "html"
|
83
94
|
```
|
95
|
+
|
96
|
+
## Templating
|
97
|
+
|
98
|
+
You may change the structure of the files output by Fyodor by providing your own template in any format you wish.
|
99
|
+
|
100
|
+
To do that, place a ERB template at `~/.config/fyodor/template.erb` or `$XDG_CONFIG_HOME/fyodor/template.erb` and Fyodor will use it automatically.
|
101
|
+
|
102
|
+
The default template can be found [here](share/template.erb). You can use any method or attribute available [on this class](lib/fyodor/output_generator.rb).
|
103
|
+
|
104
|
+
|
105
|
+
## Usage
|
106
|
+
|
107
|
+
```shell-session
|
84
108
|
$ fyodor CLIPPINGS_FILE [OUTPUT_DIR]
|
85
109
|
```
|
86
110
|
|
87
111
|
Where:
|
88
112
|
|
89
113
|
- `CLIPPINGS_FILE` is the path for `My Clippings.txt`.
|
90
|
-
- `OUTPUT_DIR` is the directory to write the
|
114
|
+
- `OUTPUT_DIR` is the directory to write the output files. If none is supplied, this will be `fyodor_output` under the current directory.
|
91
115
|
|
92
116
|
## PSA: HTML to markdown
|
93
117
|
|
@@ -105,4 +129,4 @@ If you like Fyodor, you can show your support here:
|
|
105
129
|
|
106
130
|
Licensed under [GPLv3](LICENSE)
|
107
131
|
|
108
|
-
Copyright (C) 2019-
|
132
|
+
Copyright (C) 2019-2022 [Rafael Cavalcanti](https://rafaelc.org/dev)
|
data/lib/fyodor/book.rb
CHANGED
@@ -15,7 +15,7 @@ module Fyodor
|
|
15
15
|
|
16
16
|
@title = title
|
17
17
|
@author = author
|
18
|
-
@entries =
|
18
|
+
@entries = Set.new
|
19
19
|
@rej_dup = 0
|
20
20
|
end
|
21
21
|
|
@@ -26,8 +26,8 @@ module Fyodor
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def basename
|
29
|
-
base = @author.to_s.empty? ? @title : "#{@author} - #{@title}"
|
30
|
-
base.strip
|
29
|
+
base = @author.to_s.empty? ? "Author N/A - #{@title}" : "#{@author} - #{@title}"
|
30
|
+
base.strip
|
31
31
|
end
|
32
32
|
|
33
33
|
def count_types
|
data/lib/fyodor/config_getter.rb
CHANGED
@@ -4,7 +4,9 @@ require "toml"
|
|
4
4
|
|
5
5
|
module Fyodor
|
6
6
|
class ConfigGetter
|
7
|
-
|
7
|
+
DEFAULT_TEMPLATE_PATH = File.dirname(__FILE__) + "/../../share/template.erb"
|
8
|
+
|
9
|
+
DEFAULT_CONFIG = {
|
8
10
|
"parser" => {
|
9
11
|
"highlight" => "Your Highlight",
|
10
12
|
"note" => "Your Note",
|
@@ -15,43 +17,54 @@ module Fyodor
|
|
15
17
|
"time" => "Added on"
|
16
18
|
},
|
17
19
|
"output" => {
|
18
|
-
"time" => false
|
20
|
+
"time" => false,
|
21
|
+
"extension" => "md"
|
19
22
|
}
|
20
23
|
}
|
21
24
|
|
22
25
|
def config
|
23
|
-
@config
|
26
|
+
return @config if defined?(@config)
|
27
|
+
|
28
|
+
Hash.include CoreExtensions::Hash::Merging
|
29
|
+
config = DEFAULT_CONFIG.deep_merge(user_config)
|
30
|
+
config["output"]["template"] = template
|
31
|
+
|
32
|
+
@config = config
|
24
33
|
end
|
25
34
|
|
26
35
|
|
27
36
|
private
|
28
37
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
38
|
+
def config_dir
|
39
|
+
@config_dir ||= Pathname.new(ENV["XDG_CONFIG_HOME"] || "~/.config").expand_path + "fyodor"
|
40
|
+
end
|
32
41
|
|
33
|
-
|
34
|
-
|
42
|
+
def user_config_path
|
43
|
+
config_dir + "fyodor.toml"
|
35
44
|
end
|
36
45
|
|
37
|
-
def
|
38
|
-
|
46
|
+
def user_template_path
|
47
|
+
config_dir + "template.erb"
|
39
48
|
end
|
40
49
|
|
41
|
-
def
|
42
|
-
|
50
|
+
def user_config
|
51
|
+
if user_config_path.exist?
|
52
|
+
puts "Using config at #{user_config_path}.\n"
|
53
|
+
return TOML.load_file(user_config_path)
|
54
|
+
end
|
43
55
|
|
44
|
-
|
45
|
-
|
46
|
-
@paths << Pathname.new("~/.config/fyodor/fyodor.toml").expand_path
|
56
|
+
puts "No config found: using defaults.\n"
|
57
|
+
{}
|
47
58
|
end
|
48
59
|
|
49
|
-
def
|
50
|
-
if
|
51
|
-
puts "
|
52
|
-
|
53
|
-
puts "Using config at #{path}\n\n"
|
60
|
+
def template
|
61
|
+
if user_template_path.exist?
|
62
|
+
puts "Using custom template at #{user_template_path}.\n\n"
|
63
|
+
return File.read(user_template_path)
|
54
64
|
end
|
65
|
+
|
66
|
+
puts "No custom template found: using default.\n\n"
|
67
|
+
File.read(DEFAULT_TEMPLATE_PATH)
|
55
68
|
end
|
56
69
|
end
|
57
70
|
end
|
data/lib/fyodor/entry.rb
CHANGED
@@ -48,20 +48,18 @@ module Fyodor
|
|
48
48
|
|
49
49
|
# Override this method to use a SortedSet.
|
50
50
|
def <=>(other)
|
51
|
-
return @page_start <=> other.page_start if @loc_start == 0
|
51
|
+
return (@page_start <=> other.page_start) if @loc_start == 0
|
52
52
|
|
53
53
|
@loc_start <=> other.loc_start
|
54
54
|
end
|
55
55
|
|
56
56
|
# Override the following methods for deduplication.
|
57
57
|
def ==(other)
|
58
|
-
return false if @type != other.type || @text != other.text
|
58
|
+
return false if (@type != other.type || @text != other.text)
|
59
59
|
|
60
|
-
if desc_parsed? && other.desc_parsed?
|
61
|
-
|
62
|
-
|
63
|
-
@desc == other.desc
|
64
|
-
end
|
60
|
+
return (@loc == other.loc && @page == other.page) if desc_parsed? && other.desc_parsed?
|
61
|
+
|
62
|
+
@desc == other.desc
|
65
63
|
end
|
66
64
|
|
67
65
|
alias eql? ==
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "fyodor/strings"
|
2
|
+
require "erb"
|
3
|
+
|
4
|
+
module Fyodor
|
5
|
+
class OutputGenerator
|
6
|
+
include Strings
|
7
|
+
|
8
|
+
def initialize(book, config)
|
9
|
+
@book = book
|
10
|
+
@config = config
|
11
|
+
end
|
12
|
+
|
13
|
+
def content
|
14
|
+
ERB.new(@config["template"], nil, '-').result(binding)
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def regular_entries
|
21
|
+
@book.reject { |entry| entry.type == Entry::TYPE[:bookmark] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def bookmarks
|
25
|
+
@book.select { |entry| entry.type == Entry::TYPE[:bookmark] }
|
26
|
+
end
|
27
|
+
|
28
|
+
def render_entries(entries)
|
29
|
+
output = ""
|
30
|
+
entries.sort.each do |entry|
|
31
|
+
output += "- #{item_text(entry)}\n\n"
|
32
|
+
output += " #{item_desc(entry)}\n\n" unless item_desc(entry).empty?
|
33
|
+
end
|
34
|
+
output.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
def item_text(entry)
|
38
|
+
case entry.type
|
39
|
+
when Entry::TYPE[:bookmark]
|
40
|
+
"#{page(entry)}"
|
41
|
+
when Entry::TYPE[:note]
|
42
|
+
"_Note:_ #{entry.text.strip}"
|
43
|
+
else
|
44
|
+
"#{entry.text.strip}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def item_desc(entry)
|
49
|
+
return entry.desc unless entry.desc_parsed?
|
50
|
+
|
51
|
+
case entry.type
|
52
|
+
when Entry::TYPE[:bookmark]
|
53
|
+
time(entry)
|
54
|
+
else
|
55
|
+
(type(entry) + " @ " + page(entry) + " " + time(entry)).strip
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def page(entry)
|
60
|
+
((entry.page.nil? ? "" : "page #{entry.page}, ") +
|
61
|
+
(entry.loc.nil? ? "" : "loc. #{entry.loc}")).delete_suffix(", ")
|
62
|
+
end
|
63
|
+
|
64
|
+
def time(entry)
|
65
|
+
@config["time"] ? "[#{entry.time}]" : ""
|
66
|
+
end
|
67
|
+
|
68
|
+
def type(entry)
|
69
|
+
SINGULAR[entry.type]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/fyodor/output_writer.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "fyodor/
|
1
|
+
require "fyodor/output_generator"
|
2
2
|
|
3
3
|
module Fyodor
|
4
4
|
class OutputWriter
|
@@ -12,7 +12,7 @@ module Fyodor
|
|
12
12
|
def write_all
|
13
13
|
puts "\nWriting to #{@output_dir}..." unless @library.empty?
|
14
14
|
@library.each do |book|
|
15
|
-
content =
|
15
|
+
content = OutputGenerator.new(book, @config).content
|
16
16
|
File.open(path(book), "w") { |f| f.puts(content) }
|
17
17
|
end
|
18
18
|
end
|
@@ -21,11 +21,13 @@ module Fyodor
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def path(book)
|
24
|
-
|
24
|
+
basename = book.basename.gsub(/[?*:|\/"<>]/,"_")
|
25
|
+
extension = @config["extension"]
|
26
|
+
path = @output_dir + "#{basename}.#{extension}"
|
25
27
|
|
26
28
|
i = 2
|
27
29
|
while(path.exist?)
|
28
|
-
path = @output_dir + "#{
|
30
|
+
path = @output_dir + "#{basename} - #{i}.#{extension}"
|
29
31
|
i += 1
|
30
32
|
end
|
31
33
|
|
data/lib/fyodor/version.rb
CHANGED
data/share/template.erb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
<%# Fyodor's default template. -%>
|
2
|
+
# <%= @book.basename %>
|
3
|
+
|
4
|
+
## Highlights and notes
|
5
|
+
|
6
|
+
<% if regular_entries.size > 0 -%>
|
7
|
+
<%= render_entries(regular_entries) %>
|
8
|
+
<% else -%>
|
9
|
+
None.
|
10
|
+
<% end -%>
|
11
|
+
|
12
|
+
<% if bookmarks.size > 0 -%>
|
13
|
+
## Bookmarks
|
14
|
+
|
15
|
+
<%= render_entries(bookmarks) %>
|
16
|
+
<% end -%>
|
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.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rafael Cavalcanti
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-02-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: toml
|
@@ -38,7 +38,7 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.0'
|
41
|
-
description: Parse Kindle clippings into markdown
|
41
|
+
description: Parse Kindle clippings into markdown (or any format)
|
42
42
|
email: dev@rafaelc.org
|
43
43
|
executables:
|
44
44
|
- fyodor
|
@@ -56,11 +56,12 @@ files:
|
|
56
56
|
- lib/fyodor/entry.rb
|
57
57
|
- lib/fyodor/entry_parser.rb
|
58
58
|
- lib/fyodor/library.rb
|
59
|
-
- lib/fyodor/
|
59
|
+
- lib/fyodor/output_generator.rb
|
60
60
|
- lib/fyodor/output_writer.rb
|
61
61
|
- lib/fyodor/stats_printer.rb
|
62
62
|
- lib/fyodor/strings.rb
|
63
63
|
- lib/fyodor/version.rb
|
64
|
+
- share/template.erb
|
64
65
|
homepage: https://rafaelc.org/fyodor
|
65
66
|
licenses:
|
66
67
|
- GPL-3.0-only
|
@@ -77,14 +78,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
78
|
version: '2.5'
|
78
79
|
- - "<"
|
79
80
|
- !ruby/object:Gem::Version
|
80
|
-
version: '
|
81
|
+
version: '4'
|
81
82
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
83
|
requirements:
|
83
84
|
- - ">="
|
84
85
|
- !ruby/object:Gem::Version
|
85
86
|
version: '0'
|
86
87
|
requirements: []
|
87
|
-
rubygems_version: 3.
|
88
|
+
rubygems_version: 3.2.3
|
88
89
|
signing_key:
|
89
90
|
specification_version: 4
|
90
91
|
summary: Kindle clippings parser
|
data/lib/fyodor/md_generator.rb
DELETED
@@ -1,96 +0,0 @@
|
|
1
|
-
require "fyodor/strings"
|
2
|
-
|
3
|
-
module Fyodor
|
4
|
-
class MdGenerator
|
5
|
-
include Strings
|
6
|
-
|
7
|
-
def initialize(book, config)
|
8
|
-
@book = book
|
9
|
-
@config = config
|
10
|
-
end
|
11
|
-
|
12
|
-
def content
|
13
|
-
header + body + bookmarks
|
14
|
-
end
|
15
|
-
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def header
|
20
|
-
return <<~EOF
|
21
|
-
# #{@book.title}
|
22
|
-
#{"by #{@book.author}" unless @book.author.to_s.empty?}
|
23
|
-
|
24
|
-
#{header_counts}
|
25
|
-
|
26
|
-
EOF
|
27
|
-
end
|
28
|
-
|
29
|
-
def header_counts
|
30
|
-
output = ""
|
31
|
-
@book.count_types.each do |type, n|
|
32
|
-
output += "#{n} #{pluralize(type, n)}, " if n > 0
|
33
|
-
end
|
34
|
-
output.delete_suffix(", ")
|
35
|
-
end
|
36
|
-
|
37
|
-
def pluralize(type, n)
|
38
|
-
n == 1 ? SINGULAR[type] : PLURAL[type]
|
39
|
-
end
|
40
|
-
|
41
|
-
def body
|
42
|
-
entries = @book.reject { |entry| entry.type == Entry::TYPE[:bookmark] }
|
43
|
-
entries.size == 0 ? "" : entries_render(entries)
|
44
|
-
end
|
45
|
-
|
46
|
-
def bookmarks
|
47
|
-
bookmarks = @book.select { |entry| entry.type == Entry::TYPE[:bookmark] }
|
48
|
-
bookmarks.size == 0 ? "" : entries_render(bookmarks, "Bookmarks")
|
49
|
-
end
|
50
|
-
|
51
|
-
def entries_render(entries, title=nil)
|
52
|
-
output = "---\n\n"
|
53
|
-
output += "## #{title}\n\n" unless title.nil?
|
54
|
-
entries.each do |entry|
|
55
|
-
output += "#{item_text(entry)}\n\n"
|
56
|
-
output += "<p style=\"text-align: right;\"><sup>#{item_desc(entry)}</sup></p>\n\n"
|
57
|
-
end
|
58
|
-
output
|
59
|
-
end
|
60
|
-
|
61
|
-
def item_text(entry)
|
62
|
-
case entry.type
|
63
|
-
when Entry::TYPE[:bookmark]
|
64
|
-
"* #{page(entry)}"
|
65
|
-
when Entry::TYPE[:note]
|
66
|
-
"* _Note:_\n#{entry.text.strip}"
|
67
|
-
else
|
68
|
-
"* #{entry.text.strip}"
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def item_desc(entry)
|
73
|
-
return entry.desc unless entry.desc_parsed?
|
74
|
-
|
75
|
-
case entry.type
|
76
|
-
when Entry::TYPE[:bookmark]
|
77
|
-
time(entry)
|
78
|
-
else
|
79
|
-
(type(entry) + " @ " + page(entry) + " " + time(entry)).strip
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def page(entry)
|
84
|
-
((entry.page.nil? ? "" : "page #{entry.page}, ") +
|
85
|
-
(entry.loc.nil? ? "" : "loc. #{entry.loc}")).delete_suffix(", ")
|
86
|
-
end
|
87
|
-
|
88
|
-
def time(entry)
|
89
|
-
@config["time"] ? "[#{entry.time}]" : ""
|
90
|
-
end
|
91
|
-
|
92
|
-
def type(entry)
|
93
|
-
SINGULAR[entry.type]
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|