anki_record 0.1.1 → 0.2.0
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/CHANGELOG.md +9 -2
- data/Gemfile.lock +2 -2
- data/README.md +64 -11
- data/anki_record.gemspec +1 -1
- data/lib/anki_record/anki_package.rb +35 -24
- data/lib/anki_record/card.rb +75 -0
- data/lib/anki_record/card_template.rb +37 -23
- data/lib/anki_record/collection.rb +45 -5
- data/lib/anki_record/deck.rb +19 -6
- data/lib/anki_record/deck_options_group.rb +11 -5
- data/lib/anki_record/helpers/checksum_helper.rb +20 -0
- data/lib/anki_record/note.rb +135 -0
- data/lib/anki_record/note_field.rb +25 -4
- data/lib/anki_record/note_type.rb +107 -21
- data/lib/anki_record/version.rb +1 -1
- data/lib/anki_record.rb +2 -1
- metadata +10 -8
- data/.rdoc_options +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 060b1e641776c8bf67bc20fa50029c1801ca7b367e2e038b2aa9b3024cd7b22b
|
4
|
+
data.tar.gz: 21a27f8b9d0726122bc2ba22eb6132573e161d30d73e4db866f081db87ea8409
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8fdf8b0330814f95a11dffba1fbf2d113aa97269a8eba51d3675d7a8a761e37148dc5561b5fd96eeb9c94fd8b416ae00ade70acf88b20a0e5f7a9a668c4a8a7b
|
7
|
+
data.tar.gz: de6e49c1acbb6f56b952f682be94affaa8beecfcea79fb473fed9b53f0970e40394302b48a2b2cc167315589d8200669a13f5f4721ac184a4606b8a3ef4972f2
|
data/CHANGELOG.md
CHANGED
@@ -2,8 +2,15 @@
|
|
2
2
|
|
3
3
|
## [Unreleased/0.1.0] - 02-22-2023
|
4
4
|
|
5
|
-
-
|
5
|
+
- The gem can be used to create an *.apkg zip file that successfully imports into Anki.
|
6
|
+
- Raw SQL statements can be executed against the temporary database before it is zipped.
|
6
7
|
|
7
8
|
## [0.1.1] - 02-24-2023
|
8
9
|
|
9
|
-
- Updated documentation
|
10
|
+
- Updated documentation to release the first version
|
11
|
+
|
12
|
+
## [0.2.0] - 03-05-2023
|
13
|
+
|
14
|
+
- `AnkiPackage#zip_and_close` is changed to `AnkiPackage#zip`
|
15
|
+
- Decks and note types can be accessed through the collection object
|
16
|
+
- Notes can be created, updated, and saved to the database and this also populates the corresponding records in the `cards` table
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# AnkiRecord
|
2
2
|
|
3
|
-
AnkiRecord provides
|
3
|
+
AnkiRecord is a Ruby gem which provides a programmatic interface to creating and updating Anki flashcard deck files (`*.apkg` Anki SQLite databases). **This gem is in an early stage of development--I do not recommend you use it yet.**
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
[Documentation](https://kylerego.github.io/anki_record_docs)
|
5
|
+
[API Documentation](https://kylerego.github.io/anki_record_docs)
|
8
6
|
|
9
7
|
## Installation
|
10
8
|
|
@@ -18,16 +16,60 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
18
16
|
|
19
17
|
## Usage
|
20
18
|
|
19
|
+
The Anki package object is instantiated with `AnkiRecord::AnkiPackage.new` and if passed a block, will execute the block and zip the `*.apkg` file:
|
20
|
+
|
21
21
|
```ruby
|
22
22
|
require "anki_record"
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
AnkiRecord::AnkiPackage.new(name: "test") do |apkg|
|
25
|
+
3.times do |number|
|
26
|
+
puts "#{3 - number}..."
|
27
|
+
end
|
28
|
+
puts "Countdown complete. Write any Ruby you want in here!"
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
If an exception is raised inside the block, the temporary `collection.anki2` and `collection.anki21` databases are deleted without creating a new `*.apkg` zip file, so this is the recommended way.
|
27
33
|
|
34
|
+
Alternatively, if `AnkiRecord::Package::new` is not passed a block, the `zip` method must be explicitly called on the Anki package object:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require "anki_record"
|
38
|
+
|
39
|
+
apkg = AnkiRecord::AnkiPackage.new(name: "test")
|
40
|
+
apkg.zip
|
28
41
|
```
|
29
42
|
|
30
|
-
|
43
|
+
A new Anki package object is initialized with the "Default" deck and the default note types of a new Anki collection (including "Basic" and "Cloze"). The deck and note type objects can be accessed through the `collection` attribute of the Anki package object through the `find_deck_by` and `find_note_type_by` methods by passing the `name` keyword argument:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require "anki_record"
|
47
|
+
|
48
|
+
apkg = AnkiRecord::AnkiPackage.new name: "test"
|
49
|
+
|
50
|
+
deck = apkg.collection.find_deck_by name: "Default"
|
51
|
+
|
52
|
+
note_type = apkg.collection.find_note_type_by name: "Basic"
|
53
|
+
|
54
|
+
note = AnkiRecord::Note.new note_type: note_type, deck: deck
|
55
|
+
note.front = "Hello"
|
56
|
+
note.back = "World"
|
57
|
+
note.save
|
58
|
+
|
59
|
+
note_type2 = apkg.collection.find_note_type_by name: "Cloze"
|
60
|
+
|
61
|
+
note2 = AnkiRecord::Note.new note_type: note_type2, deck: deck
|
62
|
+
note2.text = "Cloze {{c1::Hello}}"
|
63
|
+
note2.back_extra = "World"
|
64
|
+
note2.save
|
65
|
+
|
66
|
+
apkg.zip
|
67
|
+
|
68
|
+
```
|
69
|
+
|
70
|
+
This example creates a `test.apkg` zip file in the current working directory, which when imported into Anki, will add one Basic note and one Cloze note.
|
71
|
+
|
72
|
+
The RSpec examples are intended to provide executable documentation, and reading them may be helpful to understand the API (e.g. [anki_package_spec.rb](https://github.com/KyleRego/anki_record/blob/main/spec/anki_record/anki_package_spec.rb)).
|
31
73
|
|
32
74
|
## Development
|
33
75
|
|
@@ -35,10 +77,21 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
35
77
|
|
36
78
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
37
79
|
|
80
|
+
### Development road map:
|
81
|
+
- Work on creating and updating notes and cards to the collection.anki21 database
|
82
|
+
- Validation logic of what makes the note valid based on the note type's card templates and fields
|
83
|
+
- Work on adding media support
|
84
|
+
- Checksum for notes needs to be updated
|
85
|
+
- Work on updating and saving decks
|
86
|
+
- Work on updating and saving deck options groups
|
87
|
+
- Work on updating and saving note types including the note fields and card templates
|
88
|
+
|
38
89
|
### Release checklist
|
39
|
-
- Bump version
|
40
90
|
- Update changelog
|
41
|
-
-
|
91
|
+
- Update usage examples
|
92
|
+
- Update and regenerate documentation
|
93
|
+
- Bump version
|
94
|
+
- Release gem
|
42
95
|
|
43
96
|
<!-- ## Contributing
|
44
97
|
|
@@ -50,4 +103,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
50
103
|
|
51
104
|
## Code of Conduct
|
52
105
|
|
53
|
-
Everyone interacting in the AnkiRecord project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
106
|
+
Everyone interacting in the AnkiRecord project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/KyleRego/anki_record/blob/main/CODE_OF_CONDUCT.md).
|
data/anki_record.gemspec
CHANGED
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.require_paths = ["lib"]
|
35
35
|
|
36
36
|
spec.add_dependency "rubyzip", ">= 2.3"
|
37
|
-
spec.add_dependency "sqlite3"
|
37
|
+
spec.add_dependency "sqlite3", "~> 1.3"
|
38
38
|
|
39
39
|
# For more information and examples about making a new gem, check out our
|
40
40
|
# guide at: https://bundler.io/guides/creating_gem.html
|
@@ -3,6 +3,9 @@
|
|
3
3
|
require "pry"
|
4
4
|
require "pathname"
|
5
5
|
|
6
|
+
require_relative "card"
|
7
|
+
require_relative "note"
|
8
|
+
|
6
9
|
require_relative "db/anki_schema_definition"
|
7
10
|
require_relative "db/clean_collection2_record"
|
8
11
|
require_relative "db/clean_collection21_record"
|
@@ -24,6 +27,10 @@ module AnkiRecord
|
|
24
27
|
|
25
28
|
private_constant :NAME_ERROR_MESSAGE, :PATH_ERROR_MESSAGE, :STANDARD_ERROR_MESSAGE
|
26
29
|
|
30
|
+
##
|
31
|
+
# The collection object of the package
|
32
|
+
attr_reader :collection
|
33
|
+
|
27
34
|
##
|
28
35
|
# Creates a new object which represents an Anki SQLite3 database
|
29
36
|
#
|
@@ -34,20 +41,11 @@ module AnkiRecord
|
|
34
41
|
# which is saved in +directory+. +directory+ is the current working directory by default.
|
35
42
|
# If the block throws a runtime error, the temporary files are deleted but the zip file is not created.
|
36
43
|
#
|
37
|
-
# When no block argument is used, #
|
38
|
-
def initialize(name:, directory: Dir.pwd)
|
44
|
+
# When no block argument is used, #zip must be called explicitly at the end of your script.
|
45
|
+
def initialize(name:, directory: Dir.pwd, &closure)
|
39
46
|
setup_package_instance_variables(name: name, directory: directory)
|
40
47
|
|
41
|
-
|
42
|
-
|
43
|
-
begin
|
44
|
-
yield self
|
45
|
-
rescue StandardError => e
|
46
|
-
close
|
47
|
-
puts_error_and_standard_message(error: e)
|
48
|
-
else
|
49
|
-
zip_and_close
|
50
|
-
end
|
48
|
+
execute_closure_and_zip(self, &closure) if block_given?
|
51
49
|
end
|
52
50
|
|
53
51
|
##
|
@@ -62,6 +60,15 @@ module AnkiRecord
|
|
62
60
|
|
63
61
|
private
|
64
62
|
|
63
|
+
def execute_closure_and_zip(object_to_yield, &closure)
|
64
|
+
closure.call(object_to_yield)
|
65
|
+
rescue StandardError => e
|
66
|
+
destroy_temporary_directory
|
67
|
+
puts_error_and_standard_message(error: e)
|
68
|
+
else
|
69
|
+
zip
|
70
|
+
end
|
71
|
+
|
65
72
|
def setup_package_instance_variables(name:, directory:)
|
66
73
|
@name = check_name_is_valid(name: name)
|
67
74
|
@directory = directory # TODO: check directory is valid
|
@@ -118,13 +125,22 @@ module AnkiRecord
|
|
118
125
|
# Creates a new object which represents the Anki SQLite3 database file at +path+
|
119
126
|
#
|
120
127
|
# Development has focused on ::new so this method is not recommended at this time
|
121
|
-
def self.open(path:,
|
128
|
+
def self.open(path:, target_directory: nil, &closure)
|
122
129
|
pathname = check_file_at_path_is_valid(path: path)
|
123
|
-
|
124
|
-
|
130
|
+
new_apkg_name = "#{File.basename(pathname.to_s, ".apkg")}-#{seconds_since_epoch}"
|
131
|
+
|
132
|
+
@anki_package = if target_directory
|
133
|
+
new(name: new_apkg_name, directory: target_directory)
|
134
|
+
else
|
135
|
+
new(name: new_apkg_name)
|
136
|
+
end
|
137
|
+
@anki_package.send :execute_closure_and_zip, @anki_package, &closure if block_given?
|
138
|
+
@anki_package
|
125
139
|
end
|
126
140
|
|
127
141
|
class << self
|
142
|
+
include TimeHelper
|
143
|
+
|
128
144
|
private
|
129
145
|
|
130
146
|
def check_file_at_path_is_valid(path:)
|
@@ -133,22 +149,17 @@ module AnkiRecord
|
|
133
149
|
|
134
150
|
pathname
|
135
151
|
end
|
136
|
-
|
137
|
-
def copy_apkg_file(pathname:)
|
138
|
-
path = pathname.to_s
|
139
|
-
FileUtils.cp path, "#{path}.copy-#{Time.now.to_i}"
|
140
|
-
end
|
141
152
|
end
|
142
153
|
|
143
154
|
##
|
144
155
|
# Zips the temporary files into the *.apkg package and deletes the temporary files.
|
145
|
-
def
|
146
|
-
|
156
|
+
def zip
|
157
|
+
create_zip_file && destroy_temporary_directory
|
147
158
|
end
|
148
159
|
|
149
160
|
private
|
150
161
|
|
151
|
-
def
|
162
|
+
def create_zip_file
|
152
163
|
Zip::File.open(target_zip_file, create: true) do |zip_file|
|
153
164
|
@tmp_files.each do |file_name|
|
154
165
|
zip_file.add(file_name, File.join(@tmpdir, file_name))
|
@@ -161,7 +172,7 @@ module AnkiRecord
|
|
161
172
|
"#{@directory}/#{@name}.apkg"
|
162
173
|
end
|
163
174
|
|
164
|
-
def
|
175
|
+
def destroy_temporary_directory
|
165
176
|
@anki21_database.close
|
166
177
|
FileUtils.rm_rf(@tmpdir)
|
167
178
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pry"
|
4
|
+
|
5
|
+
require_relative "helpers/shared_constants_helper"
|
6
|
+
require_relative "helpers/time_helper"
|
7
|
+
|
8
|
+
module AnkiRecord
|
9
|
+
##
|
10
|
+
# Card represents an Anki card.
|
11
|
+
class Card
|
12
|
+
include TimeHelper
|
13
|
+
include SharedConstantsHelper
|
14
|
+
|
15
|
+
##
|
16
|
+
# The note that the card belongs to
|
17
|
+
attr_reader :note
|
18
|
+
|
19
|
+
##
|
20
|
+
# The card template that the card uses
|
21
|
+
attr_reader :card_template
|
22
|
+
|
23
|
+
##
|
24
|
+
# The id of the card, which is time it was created as the number of milliseconds since the 1970 epoch
|
25
|
+
attr_reader :id
|
26
|
+
|
27
|
+
##
|
28
|
+
# The time that the card was last modified as the number of seconds since the 1970 epoch
|
29
|
+
attr_reader :last_modified_time
|
30
|
+
|
31
|
+
# rubocop:disable Metrics/MethodLength
|
32
|
+
# rubocop:disable Metrics/AbcSize
|
33
|
+
def initialize(note:, card_template:)
|
34
|
+
raise ArgumentError unless note && card_template && note.note_type == card_template.note_type
|
35
|
+
|
36
|
+
@note = note
|
37
|
+
@apkg = @note.deck.collection.anki_package
|
38
|
+
|
39
|
+
@card_template = card_template
|
40
|
+
|
41
|
+
@id = milliseconds_since_epoch
|
42
|
+
@last_modified_time = seconds_since_epoch
|
43
|
+
@usn = NEW_OBJECT_USN
|
44
|
+
@type = 0
|
45
|
+
@queue = 0
|
46
|
+
@due = 0
|
47
|
+
@ivl = 0
|
48
|
+
@factor = 0
|
49
|
+
@reps = 0
|
50
|
+
@lapses = 0
|
51
|
+
@left = 0
|
52
|
+
@odue = 0
|
53
|
+
@odid = 0
|
54
|
+
@flags = 0
|
55
|
+
@data = {}
|
56
|
+
end
|
57
|
+
# rubocop:enable Metrics/MethodLength
|
58
|
+
# rubocop:enable Metrics/AbcSize
|
59
|
+
|
60
|
+
##
|
61
|
+
# Saves the card to the collection.anki21 database
|
62
|
+
def save
|
63
|
+
@apkg.execute <<~SQL
|
64
|
+
insert into cards (id, nid, did, ord,
|
65
|
+
mod, usn, type, queue,
|
66
|
+
due, ivl, factor, reps,
|
67
|
+
lapses, left, odue, odid, flags, data)
|
68
|
+
values ('#{@id}', '#{@note.id}', '#{@note.deck.id}', '#{@card_template.ordinal_number}',
|
69
|
+
'#{@last_modified_time}', '#{@usn}', '#{@type}', '#{@queue}',
|
70
|
+
'#{@due}', '#{@ivl}', '#{@factor}', '#{@reps}',
|
71
|
+
'#{@lapses}', '#{@left}', '#{@odue}', '#{@odid}', '#{@flags}', '#{@data}')
|
72
|
+
SQL
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -9,27 +9,53 @@ module AnkiRecord
|
|
9
9
|
# CardTemplate represents a card template of an Anki note type
|
10
10
|
class CardTemplate
|
11
11
|
##
|
12
|
-
# The name of
|
12
|
+
# The name of the card template
|
13
13
|
attr_accessor :name
|
14
14
|
|
15
15
|
##
|
16
|
-
# The
|
17
|
-
attr_accessor :
|
16
|
+
# The font style shown for the card template in the browser
|
17
|
+
attr_accessor :browser_font_style
|
18
18
|
|
19
19
|
##
|
20
|
-
# The
|
21
|
-
attr_accessor :
|
20
|
+
# The font size used for the card template in the browser
|
21
|
+
attr_accessor :browser_font_size
|
22
22
|
|
23
23
|
##
|
24
|
-
# The
|
25
|
-
|
24
|
+
# The question format of the card template
|
25
|
+
attr_reader :question_format
|
26
26
|
|
27
27
|
##
|
28
|
-
#
|
29
|
-
|
28
|
+
# Sets the question format and raises an ArgumentError if the specified format uses invalid fields
|
29
|
+
def question_format=(format)
|
30
|
+
fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
|
31
|
+
capture.chomp("}}").reverse.chomp("{{").reverse
|
32
|
+
end
|
33
|
+
raise ArgumentError if fields_in_specified_format.any? do |field_name|
|
34
|
+
!note_type.allowed_card_template_question_format_field_names.include?(field_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
@question_format = format
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# The answer format of the card template
|
42
|
+
attr_reader :answer_format
|
43
|
+
|
44
|
+
##
|
45
|
+
# Sets the answer format and raises an ArgumentError if the specified format uses invalid fields
|
46
|
+
def answer_format=(format)
|
47
|
+
fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
|
48
|
+
capture.chomp("}}").reverse.chomp("{{").reverse
|
49
|
+
end
|
50
|
+
raise ArgumentError if fields_in_specified_format.any? do |field_name|
|
51
|
+
!note_type.allowed_card_template_answer_format_field_names.include?(field_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
@answer_format = format
|
55
|
+
end
|
30
56
|
|
31
57
|
##
|
32
|
-
# The note type that
|
58
|
+
# The note type that the card template belongs to
|
33
59
|
attr_reader :note_type
|
34
60
|
|
35
61
|
##
|
@@ -66,7 +92,7 @@ module AnkiRecord
|
|
66
92
|
|
67
93
|
def setup_card_template_instance_variables(name:)
|
68
94
|
@name = name
|
69
|
-
@ordinal_number = @note_type.
|
95
|
+
@ordinal_number = @note_type.card_templates.length
|
70
96
|
@question_format = ""
|
71
97
|
@answer_format = ""
|
72
98
|
@bqfmt = ""
|
@@ -75,17 +101,5 @@ module AnkiRecord
|
|
75
101
|
@browser_font_style = ""
|
76
102
|
@browser_font_size = 0
|
77
103
|
end
|
78
|
-
|
79
|
-
public
|
80
|
-
|
81
|
-
##
|
82
|
-
# Returns the field names that are allowed in the answer format and question format
|
83
|
-
#
|
84
|
-
# These are the field_name values in {{field_name}} in those formats.
|
85
|
-
#
|
86
|
-
# They are equivalent to the names of the fields of the template's note type.
|
87
|
-
def allowed_field_names
|
88
|
-
@note_type.fields.map(&:name)
|
89
|
-
end
|
90
104
|
end
|
91
105
|
end
|
@@ -15,19 +15,59 @@ module AnkiRecord
|
|
15
15
|
include AnkiRecord::TimeHelper
|
16
16
|
|
17
17
|
##
|
18
|
-
#
|
18
|
+
# The instance of AnkiRecord::AnkiPackage that this collection object belongs to
|
19
|
+
attr_reader :anki_package
|
20
|
+
|
21
|
+
##
|
22
|
+
# The id attribute will become, or is the same as, the primary key id of this record in the database
|
23
|
+
#
|
24
|
+
# Since there should be only one col record, this attribute should be 1
|
25
|
+
attr_reader :id
|
26
|
+
|
27
|
+
##
|
28
|
+
# The time in milliseconds that the col record was created since the 1970 epoch
|
29
|
+
attr_reader :creation_timestamp
|
30
|
+
|
31
|
+
##
|
32
|
+
# The last time that the col record was modified in milliseconds since the 1970 epoch
|
33
|
+
attr_reader :last_modified_time
|
34
|
+
|
35
|
+
##
|
36
|
+
# An array of the collection's note type objects, which are instances of AnkiRecord::NoteType
|
19
37
|
attr_reader :note_types
|
20
38
|
|
21
39
|
##
|
22
|
-
# An array of the collection's deck objects
|
40
|
+
# An array of the collection's deck objects, which are instances of AnkiRecord::Deck
|
23
41
|
attr_reader :decks
|
24
42
|
|
43
|
+
##
|
44
|
+
# An array of the collection's deck options group objects, which are instances of AnkiRecord::DeckOptionsGroup
|
45
|
+
#
|
46
|
+
# These represent groups of settings that can be applied to a deck.
|
47
|
+
attr_reader :deck_options_groups
|
48
|
+
|
25
49
|
##
|
26
50
|
# Instantiates the collection object for the +anki_package+
|
51
|
+
#
|
52
|
+
# The collection object represents the single record of the collection.anki21 database col table.
|
53
|
+
#
|
54
|
+
# This record stores the note types used by the notes and the decks that they belong to.
|
27
55
|
def initialize(anki_package:)
|
28
56
|
setup_collection_instance_variables(anki_package: anki_package)
|
29
57
|
end
|
30
58
|
|
59
|
+
##
|
60
|
+
# Find one of the collection's note types by name
|
61
|
+
def find_note_type_by(name: nil)
|
62
|
+
note_types.find { |note_type| note_type.name == name }
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Find one of the collection's decks by name
|
67
|
+
def find_deck_by(name: nil)
|
68
|
+
decks.find { |deck| deck.name == name }
|
69
|
+
end
|
70
|
+
|
31
71
|
private
|
32
72
|
|
33
73
|
# rubocop:disable Metrics/MethodLength
|
@@ -35,8 +75,8 @@ module AnkiRecord
|
|
35
75
|
def setup_collection_instance_variables(anki_package:)
|
36
76
|
@anki_package = anki_package
|
37
77
|
@id = col_record["id"]
|
38
|
-
@
|
39
|
-
@last_modified_time =
|
78
|
+
@creation_timestamp = col_record["crt"]
|
79
|
+
@last_modified_time = col_record["mod"]
|
40
80
|
@scm = col_record["scm"]
|
41
81
|
@ver = col_record["ver"]
|
42
82
|
@dty = col_record["dty"]
|
@@ -49,7 +89,7 @@ module AnkiRecord
|
|
49
89
|
@decks = JSON.parse(col_record["decks"]).values.map do |deck_hash|
|
50
90
|
Deck.new(collection: self, args: deck_hash)
|
51
91
|
end
|
52
|
-
@
|
92
|
+
@deck_options_groups = JSON.parse(col_record["dconf"]).values.map do |dconf_hash|
|
53
93
|
DeckOptionsGroup.new(collection: self, args: dconf_hash)
|
54
94
|
end
|
55
95
|
@tags = JSON.parse(col_record["tags"])
|
data/lib/anki_record/deck.rb
CHANGED
@@ -5,8 +5,6 @@ require "pry"
|
|
5
5
|
require_relative "helpers/shared_constants_helper"
|
6
6
|
require_relative "helpers/time_helper"
|
7
7
|
|
8
|
-
# TODO: All instance variables should at least be readable
|
9
|
-
|
10
8
|
module AnkiRecord
|
11
9
|
##
|
12
10
|
# Deck represents an Anki deck
|
@@ -19,6 +17,10 @@ module AnkiRecord
|
|
19
17
|
|
20
18
|
private_constant :DEFAULT_DECK_TODAY_ARRAY, :DEFAULT_COLLAPSED
|
21
19
|
|
20
|
+
##
|
21
|
+
# The collection object that the deck belongs to
|
22
|
+
attr_reader :collection
|
23
|
+
|
22
24
|
##
|
23
25
|
# The name of the deck
|
24
26
|
attr_accessor :name
|
@@ -28,11 +30,21 @@ module AnkiRecord
|
|
28
30
|
attr_accessor :description
|
29
31
|
|
30
32
|
##
|
31
|
-
#
|
32
|
-
attr_reader :
|
33
|
+
# The id of the deck
|
34
|
+
attr_reader :id
|
35
|
+
|
36
|
+
##
|
37
|
+
# The last time the deck was modified in number of seconds since the epoch
|
38
|
+
#
|
39
|
+
# TODO: is this really supposed to be seconds? Should it be milliseconds?
|
40
|
+
attr_reader :last_modified_time
|
41
|
+
|
42
|
+
##
|
43
|
+
# The id of the eck options/settings group that is applied to the deck
|
44
|
+
attr_reader :deck_options_group_id
|
33
45
|
|
34
46
|
##
|
35
|
-
#
|
47
|
+
# Instantiates a new Deck object
|
36
48
|
def initialize(collection:, name: nil, args: nil)
|
37
49
|
raise ArgumentError unless (name && args.nil?) || (args && args["name"])
|
38
50
|
|
@@ -79,7 +91,8 @@ module AnkiRecord
|
|
79
91
|
@collapsed_in_browser = DEFAULT_COLLAPSED
|
80
92
|
@description = ""
|
81
93
|
@dyn = NON_FILTERED_DECK_DYN
|
82
|
-
@deck_options_group_id = nil # TODO
|
94
|
+
@deck_options_group_id = nil # TODO: Set id to the default deck options group?
|
95
|
+
# TODO: alternatively, if this is nil when the deck is saved, it can be set to the default options group id
|
83
96
|
@extend_new = 0
|
84
97
|
@extend_review = 0
|
85
98
|
end
|
@@ -13,14 +13,20 @@ module AnkiRecord
|
|
13
13
|
include TimeHelper
|
14
14
|
|
15
15
|
##
|
16
|
-
# The
|
16
|
+
# The collection object that the deck options group belongs to
|
17
|
+
attr_reader :collection
|
18
|
+
|
19
|
+
##
|
20
|
+
# The name of the deck options group
|
17
21
|
attr_accessor :name
|
18
22
|
|
19
23
|
##
|
20
|
-
#
|
21
|
-
attr_reader :
|
22
|
-
|
23
|
-
|
24
|
+
# The id of the deck options group
|
25
|
+
attr_reader :id
|
26
|
+
|
27
|
+
##
|
28
|
+
# The last time that this deck options group was modified in milliseconds since the 1970 epoch
|
29
|
+
attr_reader :last_modified_time
|
24
30
|
|
25
31
|
##
|
26
32
|
# Instantiates a new deck options group called +name+ with defaults
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module AnkiRecord
|
6
|
+
##
|
7
|
+
# A module for the method that calculates the checksum value of notes.
|
8
|
+
#
|
9
|
+
# This checksum is used by Anki to detect duplicates.
|
10
|
+
module ChecksumHelper
|
11
|
+
##
|
12
|
+
# Compute the integer representation of the first 8 characters of the digest
|
13
|
+
# (calculated using the SHA-1 Secure Hash Algorithm) of the argument
|
14
|
+
# TODO: This needs to be expanded to strip HTML (except media)
|
15
|
+
# and more tests to ensure it calculates the same value as Anki does in that case
|
16
|
+
def checksum(sfld)
|
17
|
+
Digest::SHA1.hexdigest(sfld)[0...8].to_i(16).to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pry"
|
4
|
+
require "securerandom"
|
5
|
+
|
6
|
+
require_relative "helpers/checksum_helper"
|
7
|
+
require_relative "helpers/time_helper"
|
8
|
+
|
9
|
+
module AnkiRecord
|
10
|
+
##
|
11
|
+
# Note represents an Anki note
|
12
|
+
class Note
|
13
|
+
include ChecksumHelper
|
14
|
+
include TimeHelper
|
15
|
+
include SharedConstantsHelper
|
16
|
+
|
17
|
+
##
|
18
|
+
# The id of the note
|
19
|
+
attr_reader :id
|
20
|
+
|
21
|
+
##
|
22
|
+
# The globally unique id of the note
|
23
|
+
attr_reader :guid
|
24
|
+
|
25
|
+
##
|
26
|
+
# The last time the note was modified in seconds since the 1970 epoch
|
27
|
+
attr_reader :last_modified_time
|
28
|
+
|
29
|
+
##
|
30
|
+
# The tags applied to the note
|
31
|
+
#
|
32
|
+
# TODO: a setter method for the tags of the note
|
33
|
+
attr_reader :tags
|
34
|
+
|
35
|
+
##
|
36
|
+
# The deck that the note's cards will be put into when saved
|
37
|
+
attr_reader :deck
|
38
|
+
|
39
|
+
##
|
40
|
+
# The note type of the note
|
41
|
+
attr_reader :note_type
|
42
|
+
|
43
|
+
##
|
44
|
+
# The card objects of the note
|
45
|
+
attr_reader :cards
|
46
|
+
|
47
|
+
##
|
48
|
+
# Instantiate a new note for a deck and note type
|
49
|
+
# or TODO: instantiate a new object from an already existing record
|
50
|
+
# rubocop:disable Metrics/MethodLength
|
51
|
+
# rubocop:disable Metrics/AbcSize
|
52
|
+
def initialize(deck:, note_type:)
|
53
|
+
raise ArgumentError unless deck && note_type && deck.collection == note_type.collection
|
54
|
+
|
55
|
+
@apkg = deck.collection.anki_package
|
56
|
+
|
57
|
+
@id = milliseconds_since_epoch
|
58
|
+
@guid = globally_unique_id
|
59
|
+
@last_modified_time = seconds_since_epoch
|
60
|
+
@usn = NEW_OBJECT_USN
|
61
|
+
@tags = []
|
62
|
+
@deck = deck
|
63
|
+
@note_type = note_type
|
64
|
+
@field_contents = setup_field_contents
|
65
|
+
@cards = @note_type.card_templates.map { |card_template| Card.new(note: self, card_template: card_template) }
|
66
|
+
end
|
67
|
+
# rubocop:enable Metrics/MethodLength
|
68
|
+
# rubocop:enable Metrics/AbcSize
|
69
|
+
|
70
|
+
##
|
71
|
+
# Save the note to the collection.anki21 database
|
72
|
+
def save
|
73
|
+
@apkg.execute <<~SQL
|
74
|
+
insert into notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
|
75
|
+
values ('#{@id}', '#{@guid}', '#{note_type.id}', '#{@last_modified_time}', '#{@usn}', '#{@tags.join(" ")}', '#{field_values_separated_by_us}', '#{sort_field_value}', '#{checksum(sort_field_value)}', '0', '')
|
76
|
+
SQL
|
77
|
+
cards.each(&:save)
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# This overrides BasicObject#method_missing and has the effect of creating "ghost methods"
|
83
|
+
#
|
84
|
+
# Specifically, creates setter and getter ghost methods for the fields of the note's note type
|
85
|
+
#
|
86
|
+
# TODO: This should raise a NoMethodError if
|
87
|
+
# the missing method does not end with '=' and is not a field of the note type
|
88
|
+
def method_missing(method_name, field_content = nil)
|
89
|
+
method_name = method_name.to_s
|
90
|
+
return @field_contents[method_name] unless method_name.end_with?("=")
|
91
|
+
|
92
|
+
method_name = method_name.chomp("=")
|
93
|
+
valid_fields_snake_names = @field_contents.keys
|
94
|
+
unless valid_fields_snake_names.include?(method_name)
|
95
|
+
raise ArgumentError, "Valid fields for the #{note_type.name} note type are one of #{valid_fields_snake_names.join(", ")}"
|
96
|
+
end
|
97
|
+
|
98
|
+
@field_contents[method_name] = field_content
|
99
|
+
end
|
100
|
+
|
101
|
+
##
|
102
|
+
# This allows #respond_to? to be accurate for the ghost methods created by #method_missing
|
103
|
+
def respond_to_missing?(method_name, *)
|
104
|
+
method_name = method_name.to_s
|
105
|
+
if method_name.end_with?("=")
|
106
|
+
note_type.snake_case_field_names.include?(method_name.chomp("="))
|
107
|
+
else
|
108
|
+
note_type.snake_case_field_names.include?(method_name)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def setup_field_contents
|
115
|
+
field_contents = {}
|
116
|
+
note_type.snake_case_field_names.each do |field_name|
|
117
|
+
field_contents[field_name] = ""
|
118
|
+
end
|
119
|
+
field_contents
|
120
|
+
end
|
121
|
+
|
122
|
+
def globally_unique_id
|
123
|
+
SecureRandom.uuid.slice(5...15)
|
124
|
+
end
|
125
|
+
|
126
|
+
def field_values_separated_by_us
|
127
|
+
# The ASCII control code represented by hexadecimal 1F is the Unit Separator (US)
|
128
|
+
note_type.snake_case_field_names.map { |field_name| @field_contents[field_name] }.join("\x1F")
|
129
|
+
end
|
130
|
+
|
131
|
+
def sort_field_value
|
132
|
+
@field_contents[note_type.snake_case_sort_field_name]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -14,17 +14,37 @@ module AnkiRecord
|
|
14
14
|
DEFAULT_FIELD_DESCRIPTION = ""
|
15
15
|
private_constant :DEFAULT_FIELD_FONT_STYLE, :DEFAULT_FIELD_FONT_SIZE, :DEFAULT_FIELD_DESCRIPTION
|
16
16
|
|
17
|
+
##
|
18
|
+
# The note type that the note field belongs to
|
19
|
+
attr_reader :note_type
|
20
|
+
|
17
21
|
##
|
18
22
|
# The name of the note field
|
19
23
|
attr_accessor :name
|
20
24
|
|
21
25
|
##
|
22
|
-
#
|
23
|
-
attr_accessor :sticky
|
26
|
+
# A boolean that indicates if this field is sticky when adding cards of the note type in Anki
|
27
|
+
attr_accessor :sticky
|
28
|
+
|
29
|
+
##
|
30
|
+
# A boolean that indicates if this field should be right to left
|
31
|
+
attr_accessor :right_to_left
|
32
|
+
|
33
|
+
##
|
34
|
+
# The font style used when editing the note field
|
35
|
+
attr_accessor :font_style
|
36
|
+
|
37
|
+
##
|
38
|
+
# The font size used when editing the note field
|
39
|
+
attr_accessor :font_size
|
40
|
+
|
41
|
+
##
|
42
|
+
# The description of the note field
|
43
|
+
attr_accessor :description
|
24
44
|
|
25
45
|
##
|
26
|
-
#
|
27
|
-
attr_reader :
|
46
|
+
# 0 for the first field of the note type, 1 for the second, etc.
|
47
|
+
attr_reader :ordinal_number
|
28
48
|
|
29
49
|
##
|
30
50
|
# Instantiates a new field for the given note type
|
@@ -48,6 +68,7 @@ module AnkiRecord
|
|
48
68
|
@right_to_left = args["rtl"]
|
49
69
|
@font_style = args["font"]
|
50
70
|
@font_size = args["size"]
|
71
|
+
@description = args["description"]
|
51
72
|
end
|
52
73
|
|
53
74
|
def setup_note_field_instance_variables(name:)
|
@@ -7,8 +7,6 @@ require_relative "helpers/shared_constants_helper"
|
|
7
7
|
require_relative "helpers/time_helper"
|
8
8
|
require_relative "note_field"
|
9
9
|
|
10
|
-
# TODO: All instance variables should at least be readable
|
11
|
-
|
12
10
|
module AnkiRecord
|
13
11
|
##
|
14
12
|
# NoteType represents an Anki note type (also called a model)
|
@@ -19,20 +17,54 @@ module AnkiRecord
|
|
19
17
|
private_constant :NEW_NOTE_TYPE_SORT_FIELD
|
20
18
|
|
21
19
|
##
|
22
|
-
# The
|
20
|
+
# The collection object that the note type belongs to
|
21
|
+
attr_reader :collection
|
22
|
+
|
23
|
+
##
|
24
|
+
# The id of the note type
|
25
|
+
attr_reader :id
|
26
|
+
|
27
|
+
##
|
28
|
+
# The name of the note type
|
23
29
|
attr_accessor :name
|
24
30
|
|
25
31
|
##
|
26
|
-
#
|
32
|
+
# A boolean that indicates if this note type is a cloze-deletion note type
|
27
33
|
attr_accessor :cloze
|
28
34
|
|
29
35
|
##
|
30
|
-
#
|
31
|
-
|
36
|
+
# The CSS styling of the note type
|
37
|
+
attr_reader :css
|
38
|
+
|
39
|
+
##
|
40
|
+
# The LaTeX preamble of the note type
|
41
|
+
attr_reader :latex_preamble
|
42
|
+
|
43
|
+
##
|
44
|
+
# The LaTeX postamble of the note type
|
45
|
+
attr_reader :latex_postamble
|
46
|
+
|
47
|
+
##
|
48
|
+
# A boolean probably related to something with LaTeX and SVG.
|
49
|
+
#
|
50
|
+
# TODO: Investigate what this does
|
51
|
+
attr_reader :latex_svg
|
52
|
+
|
53
|
+
##
|
54
|
+
# An array of the card template objects belonging to the note type
|
55
|
+
attr_reader :card_templates
|
32
56
|
|
33
57
|
##
|
34
|
-
#
|
35
|
-
attr_reader :
|
58
|
+
# An array of the field names of the card template
|
59
|
+
attr_reader :fields
|
60
|
+
|
61
|
+
##
|
62
|
+
# TODO: Investigate the meaning of the deck id of a note type
|
63
|
+
attr_reader :deck_id
|
64
|
+
|
65
|
+
##
|
66
|
+
# TODO: Investigate the meaning of tags of a note type
|
67
|
+
attr_reader :tags
|
36
68
|
|
37
69
|
##
|
38
70
|
# Instantiates a new note type
|
@@ -44,19 +76,19 @@ module AnkiRecord
|
|
44
76
|
if args
|
45
77
|
setup_note_type_instance_variables_from_existing(args: args)
|
46
78
|
else
|
47
|
-
setup_note_type_instance_variables(
|
48
|
-
name: name, cloze: cloze
|
49
|
-
)
|
79
|
+
setup_note_type_instance_variables(name: name, cloze: cloze)
|
50
80
|
end
|
51
81
|
end
|
52
82
|
|
53
83
|
##
|
54
|
-
#
|
84
|
+
# Creates a new field and adds it to this note type's fields
|
55
85
|
#
|
56
86
|
# The field is an instance of AnkiRecord::NoteField
|
57
87
|
def new_note_field(name:)
|
58
|
-
# TODO:
|
59
|
-
|
88
|
+
# TODO: Raise an exception if the name is already used by a field in this note type
|
89
|
+
note_field = AnkiRecord::NoteField.new(note_type: self, name: name)
|
90
|
+
@fields << note_field
|
91
|
+
note_field
|
60
92
|
end
|
61
93
|
|
62
94
|
##
|
@@ -64,8 +96,65 @@ module AnkiRecord
|
|
64
96
|
#
|
65
97
|
# The card template is an instance of AnkiRecord::CardTemplate
|
66
98
|
def new_card_template(name:)
|
67
|
-
# TODO:
|
68
|
-
|
99
|
+
# TODO: Raise an exception if the name is already used by a template in this note type
|
100
|
+
card_template = AnkiRecord::CardTemplate.new(note_type: self, name: name)
|
101
|
+
@card_templates << card_template
|
102
|
+
card_template
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Find one of the note type's card templates by name
|
107
|
+
def find_card_template_by(name:)
|
108
|
+
card_templates.find { |template| template.name == name }
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# The field names of the note type ordered by their ordinal values
|
113
|
+
def field_names_in_order
|
114
|
+
@fields.sort_by(&:ordinal_number).map(&:name)
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# The allowed field names of the note in snake_case
|
119
|
+
#
|
120
|
+
# TODO: make this more robust... what happens when the note type name has non-alphabetic characters?
|
121
|
+
def snake_case_field_names
|
122
|
+
field_names_in_order.map { |field_name| field_name.downcase.gsub(" ", "_") }
|
123
|
+
end
|
124
|
+
|
125
|
+
##
|
126
|
+
# The name of the field used to sort notes of the note type in the Anki browser
|
127
|
+
def sort_field_name
|
128
|
+
@fields.find { |field| field.ordinal_number == @sort_field }&.name
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# The name of the sort field in snake_case
|
133
|
+
def snake_case_sort_field_name
|
134
|
+
sort_field_name.downcase.gsub(" ", "_")
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# The allowed field_name values in {{field_name}} of the note type's card templates' question format
|
139
|
+
#
|
140
|
+
# These are the note type's fields' names, and if the note type is a cloze type,
|
141
|
+
# these also include the note type's fields' names prepended with 'cloze:'.
|
142
|
+
#
|
143
|
+
# TODO: research if other special field names like e.g. 'FrontSide' are allowed
|
144
|
+
def allowed_card_template_question_format_field_names
|
145
|
+
allowed = field_names_in_order
|
146
|
+
cloze ? allowed + field_names_in_order.map { |field_name| "cloze:#{field_name}" } : allowed
|
147
|
+
end
|
148
|
+
|
149
|
+
##
|
150
|
+
# The allowed field_name values in {{field_name}} of the note type's card templates' answer format
|
151
|
+
#
|
152
|
+
# These are the note type's fields' names, and if the note type is a cloze type,
|
153
|
+
# these also include the note type's fields' names prepended with 'cloze:'.
|
154
|
+
#
|
155
|
+
# TODO: research if other special field names like e.g. 'FrontSide' are allowed
|
156
|
+
def allowed_card_template_answer_format_field_names
|
157
|
+
allowed_card_template_question_format_field_names + ["FrontSide"]
|
69
158
|
end
|
70
159
|
|
71
160
|
private
|
@@ -81,7 +170,7 @@ module AnkiRecord
|
|
81
170
|
@sort_field = args["sortf"]
|
82
171
|
@deck_id = args["did"]
|
83
172
|
@fields = args["flds"].map { |fld| NoteField.new(note_type: self, args: fld) }
|
84
|
-
@
|
173
|
+
@card_templates = args["tmpls"].map { |tmpl| CardTemplate.new(note_type: self, args: tmpl) }
|
85
174
|
@css = args["css"]
|
86
175
|
@latex_preamble = args["latexPre"]
|
87
176
|
@latex_postamble = args["latexPost"]
|
@@ -103,7 +192,7 @@ module AnkiRecord
|
|
103
192
|
@sort_field = NEW_NOTE_TYPE_SORT_FIELD
|
104
193
|
@deck_id = nil
|
105
194
|
@fields = []
|
106
|
-
@
|
195
|
+
@card_templates = []
|
107
196
|
@css = default_css
|
108
197
|
@latex_preamble = default_latex_preamble
|
109
198
|
@latex_postamble = default_latex_postamble
|
@@ -114,7 +203,6 @@ module AnkiRecord
|
|
114
203
|
end
|
115
204
|
# rubocop:enable Metrics/MethodLength
|
116
205
|
|
117
|
-
# TODO: use constant here
|
118
206
|
def default_css
|
119
207
|
<<-CSS
|
120
208
|
.card {
|
@@ -125,7 +213,6 @@ module AnkiRecord
|
|
125
213
|
CSS
|
126
214
|
end
|
127
215
|
|
128
|
-
# TODO: use constant here
|
129
216
|
def default_latex_preamble
|
130
217
|
<<-LATEX_PRE
|
131
218
|
\\documentclass[12pt]{article}
|
@@ -137,7 +224,6 @@ module AnkiRecord
|
|
137
224
|
LATEX_PRE
|
138
225
|
end
|
139
226
|
|
140
|
-
# TODO: use constant here
|
141
227
|
def default_latex_postamble
|
142
228
|
<<-LATEX_POST
|
143
229
|
\\end{document}
|
data/lib/anki_record/version.rb
CHANGED
data/lib/anki_record.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "securerandom"
|
4
3
|
require "sqlite3"
|
5
4
|
require "zip"
|
6
5
|
|
@@ -10,10 +9,12 @@ require_relative "anki_record/version"
|
|
10
9
|
##
|
11
10
|
# This module is the namespace for all AnkiRecord classes:
|
12
11
|
# - AnkiPackage
|
12
|
+
# - Card
|
13
13
|
# - CardTemplate
|
14
14
|
# - Collection
|
15
15
|
# - DeckOptionsGroup
|
16
16
|
# - Deck
|
17
|
+
# - Note
|
17
18
|
# - NoteField
|
18
19
|
# - NoteType
|
19
20
|
#
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anki_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kyle Rego
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rubyzip
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: sqlite3
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1.3'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '1.3'
|
41
41
|
description: " This Ruby library, which is currently in development, will provide
|
42
42
|
an interface to inspect, update, and create Anki SQLite3 databases (*.apkg files).\n"
|
43
43
|
email:
|
@@ -46,7 +46,6 @@ executables: []
|
|
46
46
|
extensions: []
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
|
-
- ".rdoc_options"
|
50
49
|
- ".rspec"
|
51
50
|
- ".rubocop.yml"
|
52
51
|
- CHANGELOG.md
|
@@ -59,6 +58,7 @@ files:
|
|
59
58
|
- anki_record.gemspec
|
60
59
|
- lib/anki_record.rb
|
61
60
|
- lib/anki_record/anki_package.rb
|
61
|
+
- lib/anki_record/card.rb
|
62
62
|
- lib/anki_record/card_template.rb
|
63
63
|
- lib/anki_record/collection.rb
|
64
64
|
- lib/anki_record/db/anki_schema_definition.rb
|
@@ -66,8 +66,10 @@ files:
|
|
66
66
|
- lib/anki_record/db/clean_collection2_record.rb
|
67
67
|
- lib/anki_record/deck.rb
|
68
68
|
- lib/anki_record/deck_options_group.rb
|
69
|
+
- lib/anki_record/helpers/checksum_helper.rb
|
69
70
|
- lib/anki_record/helpers/shared_constants_helper.rb
|
70
71
|
- lib/anki_record/helpers/time_helper.rb
|
72
|
+
- lib/anki_record/note.rb
|
71
73
|
- lib/anki_record/note_field.rb
|
72
74
|
- lib/anki_record/note_type.rb
|
73
75
|
- lib/anki_record/version.rb
|
@@ -94,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
96
|
- !ruby/object:Gem::Version
|
95
97
|
version: '0'
|
96
98
|
requirements: []
|
97
|
-
rubygems_version: 3.4.
|
99
|
+
rubygems_version: 3.4.6
|
98
100
|
signing_key:
|
99
101
|
specification_version: 4
|
100
102
|
summary: Automate Anki flashcard editing with the Ruby programming language.
|
data/.rdoc_options
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
---
|
2
|
-
encoding: UTF-8
|
3
|
-
static_path: []
|
4
|
-
rdoc_include: []
|
5
|
-
page_dir:
|
6
|
-
charset: UTF-8
|
7
|
-
exclude:
|
8
|
-
- "~\\z"
|
9
|
-
- "\\.orig\\z"
|
10
|
-
- "\\.rej\\z"
|
11
|
-
- "\\.bak\\z"
|
12
|
-
- "\\.gemspec\\z"
|
13
|
-
hyperlink_all: false
|
14
|
-
line_numbers: false
|
15
|
-
locale:
|
16
|
-
locale_dir: locale
|
17
|
-
locale_name:
|
18
|
-
main_page:
|
19
|
-
markup: rdoc
|
20
|
-
output_decoration: true
|
21
|
-
show_hash: false
|
22
|
-
skip_tests: true
|
23
|
-
tab_width: 8
|
24
|
-
template_stylesheets: []
|
25
|
-
title:
|
26
|
-
visibility: :protected
|
27
|
-
webcvs:
|