anki_record 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|