anki_record 0.3.2 → 0.4

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rubocop.yml +2 -7
  4. data/CHANGELOG.md +9 -1
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +9 -2
  7. data/README.md +79 -132
  8. data/anki_record.gemspec +2 -2
  9. data/lib/anki_record/anki21_database/anki21_database.rb +138 -0
  10. data/lib/anki_record/anki21_database/anki21_database_attributes.rb +31 -0
  11. data/lib/anki_record/anki21_database/anki21_database_constructors.rb +52 -0
  12. data/lib/anki_record/anki2_database/anki2_database.rb +44 -0
  13. data/lib/anki_record/anki_package/anki_package.rb +110 -176
  14. data/lib/anki_record/card/card.rb +17 -33
  15. data/lib/anki_record/card/card_attributes.rb +3 -34
  16. data/lib/anki_record/card_template/card_template.rb +2 -2
  17. data/lib/anki_record/card_template/card_template_attributes.rb +11 -11
  18. data/lib/anki_record/collection/collection.rb +20 -154
  19. data/lib/anki_record/collection/collection_attributes.rb +2 -30
  20. data/lib/anki_record/deck/deck.rb +11 -10
  21. data/lib/anki_record/deck/deck_attributes.rb +6 -8
  22. data/lib/anki_record/deck/deck_defaults.rb +1 -1
  23. data/lib/anki_record/deck_options_group/deck_options_group.rb +8 -6
  24. data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +5 -7
  25. data/lib/anki_record/helpers/anki_guid_helper.rb +20 -0
  26. data/lib/anki_record/media/media.rb +36 -0
  27. data/lib/anki_record/note/note.rb +62 -86
  28. data/lib/anki_record/note/note_attributes.rb +18 -17
  29. data/lib/anki_record/note_field/note_field.rb +3 -3
  30. data/lib/anki_record/note_field/note_field_attributes.rb +9 -9
  31. data/lib/anki_record/note_type/note_type.rb +13 -14
  32. data/lib/anki_record/note_type/note_type_attributes.rb +17 -21
  33. data/lib/anki_record/version.rb +1 -1
  34. metadata +11 -7
  35. data/lib/anki_record/helpers/data_query_helper.rb +0 -15
  36. data/lib/anki_record/note/note_guid_helper.rb +0 -10
@@ -2,236 +2,170 @@
2
2
 
3
3
  require "pathname"
4
4
 
5
+ require_relative "../anki2_database/anki2_database"
6
+ require_relative "../anki21_database/anki21_database"
7
+ require_relative "../media/media"
5
8
  require_relative "../card/card"
6
9
  require_relative "../collection/collection"
7
10
  require_relative "../note/note"
8
11
  require_relative "../database_setup_constants"
9
12
 
10
- # rubocop:disable Metrics/ClassLength
11
13
  module AnkiRecord
12
14
  ##
13
- # AnkiPackage represents an Anki package deck file.
15
+ # AnkiPackage represents the Anki deck package file which has the .apkg file extension
16
+ #
17
+ # This is a zip file containing two SQLite databases (collection.anki21 and collection.anki2),
18
+ # a media file, and possibly the media (images and sound files). The gem currently does not
19
+ # have any support for adding or changing media in the Anki package.
14
20
  class AnkiPackage
15
- include AnkiRecord::Helpers::DataQueryHelper
21
+ attr_accessor :anki21_database, :anki2_database, :media, :tmpdir, :tmpfiles, :target_directory, :name # :nodoc:
16
22
 
17
23
  ##
18
- # The package's collection object.
19
- attr_reader :collection
24
+ # Creates a new Anki package file (see the README)
25
+ def self.create(name:, target_directory: Dir.pwd, &closure)
26
+ anki_package = new
27
+ anki_package.create_initialize(name:, target_directory:, &closure)
28
+ anki_package
29
+ end
20
30
 
21
- ##
22
- # Instantiates a new Anki package object.
23
- #
24
- # See the README for usage details.
25
- def initialize(name:, target_directory: Dir.pwd, data: nil, open_path: nil, &closure)
26
- check_name_argument_is_valid(name:)
27
- @name = name.end_with?(".apkg") ? name[0, name.length - 5] : name
31
+ def create_initialize(name:, target_directory: Dir.pwd, &closure) # :nodoc:
32
+ validate_arguments(name:, target_directory:)
33
+ @name = new_apkg_name(name:)
28
34
  @target_directory = target_directory
29
- @open_path = open_path
30
- check_directory_argument_is_valid
31
- setup_other_package_instance_variables
32
- insert_existing_data(data: data) if data
35
+ @tmpdir = Dir.mktmpdir
36
+ @tmpfiles = [Anki21Database::FILENAME, Anki2Database::FILENAME, Media::FILENAME]
37
+ @anki21_database = Anki21Database.create_new(anki_package: self)
38
+ @anki2_database = Anki2Database.create_new(anki_package: self)
39
+ @media = Media.create_new(anki_package: self)
33
40
 
34
- execute_closure_and_zip(collection, &closure) if block_given?
41
+ execute_closure_and_zip(anki21_database, &closure) if closure
35
42
  end
36
43
 
37
- # Returns an SQLite3::Statement object representing the given SQL and coupled to the collection.anki21 database.
38
- #
39
- # The Statement is executed using Statement#execute (see sqlite3 gem).
40
- def prepare(sql)
41
- @anki21_database.prepare sql
44
+ ##
45
+ # Opens an existing Anki package file to update its contents (see the README)
46
+ def self.update(path:, &closure)
47
+ anki_package = new
48
+ anki_package.update_initialize(path:, &closure)
49
+ anki_package
42
50
  end
43
51
 
44
- private
45
-
46
- def execute_closure_and_zip(collection, &closure)
47
- closure.call(collection)
48
- rescue StandardError => e
49
- destroy_temporary_directory
50
- puts_error_and_standard_message(error: e)
51
- else
52
- zip
53
- end
54
-
55
- def setup_other_package_instance_variables
56
- @tmpdir = Dir.mktmpdir
57
- @tmp_files = []
58
- @anki21_database = setup_anki21_database_object
59
- @anki2_database = setup_anki2_database_object
60
- @media_file = setup_media
61
- @collection = Collection.new(anki_package: self)
62
- end
52
+ def update_initialize(path:, &closure) # :nodoc:
53
+ validate_path(path:)
63
54
 
64
- def check_name_argument_is_valid(name:)
65
- return if name.instance_of?(String) && !name.empty? && !name.include?(" ")
55
+ @tmpdir = Dir.mktmpdir
56
+ unzip_apkg_into_tmpdir(path:)
57
+ @tmpfiles = [Anki21Database::FILENAME, Anki2Database::FILENAME, Media::FILENAME]
58
+ @anki21_database = Anki21Database.update_new(anki_package: self)
59
+ @anki2_database = Anki2Database.update_new(anki_package: self)
60
+ @media = Media.update_new(anki_package: self)
66
61
 
67
- raise ArgumentError, "The package name must be a string without spaces."
68
- end
62
+ @updating_existing_apkg = true
63
+ execute_closure_and_zip(anki21_database, &closure) if closure
64
+ end
69
65
 
70
- def check_directory_argument_is_valid
71
- raise ArgumentError, "No directory was found at the given path." unless File.directory?(@target_directory)
72
- end
66
+ # :nodoc:
67
+ def zip
68
+ @updating_existing_apkg ? replace_zip_file : create_zip_file
69
+ destroy_temporary_directory
70
+ end
73
71
 
74
- def setup_anki21_database_object
75
- anki21_file_name = "collection.anki21"
76
- db = SQLite3::Database.new "#{@tmpdir}/#{anki21_file_name}", options: {}
77
- @tmp_files << anki21_file_name
78
- db.execute_batch ANKI_SCHEMA_DEFINITION
79
- db.execute INSERT_COLLECTION_ANKI_21_COL_RECORD
80
- db.results_as_hash = true
81
- db
82
- end
72
+ # :nocov:
73
+ def inspect
74
+ "[= AnkiPackage name: #{name} target_directory: #{target_directory} =]"
75
+ end
76
+ # :nocov:
83
77
 
84
- def setup_anki2_database_object
85
- anki2_file_name = "collection.anki2"
86
- db = SQLite3::Database.new "#{@tmpdir}/#{anki2_file_name}", options: {}
87
- @tmp_files << anki2_file_name
88
- db.execute_batch ANKI_SCHEMA_DEFINITION
89
- db.execute INSERT_COLLECTION_ANKI_2_COL_RECORD
90
- db.close
91
- db
92
- end
78
+ private
93
79
 
94
- def setup_media
95
- media_file_path = FileUtils.touch("#{@tmpdir}/media")[0]
96
- media_file = File.open(media_file_path, mode: "w")
97
- media_file.write("{}")
98
- media_file.close
99
- @tmp_files << "media"
100
- media_file
101
- end
80
+ def validate_path(path:)
81
+ pathname = Pathname.new(path)
82
+ raise "*No .apkg file was found at the given path." unless pathname.file? && pathname.extname == ".apkg"
102
83
 
103
- def insert_existing_data(data:)
104
- @collection.copy_over_existing(col_record: data[:col_record])
105
- copy_over_notes_and_cards(note_ids: data[:note_ids])
84
+ @name = File.basename(pathname.to_s, ".apkg")
85
+ @target_directory = pathname.expand_path.dirname.to_s
106
86
  end
107
87
 
108
- def copy_over_notes_and_cards(note_ids:)
109
- temporarily_unzip_source_apkg do |source_collection_anki21|
110
- note_ids.each do |note_id|
111
- note_cards_data = note_cards_data_for_note_id(sql_able: source_collection_anki21, id: note_id)
112
- AnkiRecord::Note.new(collection: @collection, data: note_cards_data).save
88
+ def unzip_apkg_into_tmpdir(path:)
89
+ Zip::File.open(path) do |zip_file|
90
+ zip_file.each do |entry|
91
+ entry.extract("#{tmpdir}/#{entry.name}")
113
92
  end
114
93
  end
115
94
  end
116
95
 
117
- def standard_error_thrown_in_block_message
118
- "Any temporary files created have been deleted.\nNo new *.apkg zip file was saved."
96
+ def validate_arguments(name:, target_directory:)
97
+ check_name_argument_is_valid(name:)
98
+ check_target_directory_argument_is_valid(target_directory:)
119
99
  end
120
100
 
121
- def puts_error_and_standard_message(error:)
122
- puts error.backtrace
123
- puts "#{error}\n#{standard_error_thrown_in_block_message}"
124
- end
125
-
126
- public
101
+ def check_name_argument_is_valid(name:)
102
+ return if name.instance_of?(String) && !name.empty? && !name.include?(" ")
127
103
 
128
- ##
129
- # Instantiates a new Anki package object seeded with data from the opened Anki package.
130
- #
131
- # See the README for details.
132
- def self.open(path:, target_directory: nil, &closure)
133
- pathname = Pathname.new(path)
134
- raise "*No .apkg file was found at the given path." unless pathname.file? && pathname.extname == ".apkg"
135
-
136
- new_apkg_name = "#{File.basename(pathname.to_s, ".apkg")}-#{seconds_since_epoch}"
137
- data = col_record_and_note_ids_to_copy_over(pathname: pathname)
138
-
139
- if target_directory
140
- new(name: new_apkg_name, data: data, open_path: pathname,
141
- target_directory: target_directory, &closure)
142
- else
143
- new(name: new_apkg_name, data: data, open_path: pathname, &closure)
104
+ raise ArgumentError, "The package name must be a string without spaces."
144
105
  end
145
- end
146
-
147
- def was_instantiated_from_existing_apkg? # :nodoc:
148
- !@open_path.nil?
149
- end
150
-
151
- # rubocop:disable Metrics/MethodLength
152
- # :nodoc:
153
- def temporarily_unzip_source_apkg
154
- raise ArgumentError unless @open_path && block_given?
155
106
 
156
- Zip::File.open(@open_path) do |zip_file|
157
- zip_file.each do |entry|
158
- next unless entry.name == "collection.anki21"
107
+ def check_target_directory_argument_is_valid(target_directory:)
108
+ return if File.directory?(target_directory)
159
109
 
160
- entry.extract
161
- source_collection_anki21 = SQLite3::Database.open "collection.anki21"
162
- source_collection_anki21.results_as_hash = true
163
-
164
- yield source_collection_anki21
165
- end
110
+ raise ArgumentError, "No directory was found at the given path."
166
111
  end
167
- File.delete("collection.anki21")
168
- end
169
- # rubocop:enable Metrics/MethodLength
170
112
 
171
- class << self
172
- include Helpers::TimeHelper
113
+ def new_apkg_name(name:)
114
+ name.end_with?(".apkg") ? name[0, name.length - 5] : name
115
+ end
173
116
 
174
117
  # rubocop:disable Metrics/MethodLength
175
- # rubocop:disable Metrics/AbcSize
176
- def col_record_and_note_ids_to_copy_over(pathname:) # :nodoc:
177
- data = {}
178
- Zip::File.open(pathname) do |zip_file|
179
- zip_file.each do |entry|
180
- next unless entry.name == "collection.anki21"
181
-
182
- entry.extract
183
- source_collection_anki21 = SQLite3::Database.open "collection.anki21"
184
- source_collection_anki21.results_as_hash = true
185
- col_record = source_collection_anki21.prepare("select * from col").execute.first
186
- note_ids = source_collection_anki21.prepare("select id from notes").execute.map { |note| note["id"] }
187
- data = { col_record: col_record, note_ids: note_ids }
188
- end
189
- end
190
- File.delete("collection.anki21")
191
- data
118
+ def execute_closure_and_zip(anki21_database)
119
+ yield(anki21_database)
120
+ rescue StandardError => e
121
+ destroy_temporary_directory
122
+ puts e.backtrace.reverse
123
+ puts e
124
+ puts "An error occurred within the block argument."
125
+ puts "The temporary files have been deleted."
126
+ puts "If you were creating a new Anki package, nothing was saved."
127
+ puts "If you were updating an existing one, it was not changed."
128
+ else
129
+ zip
192
130
  end
193
- # rubocop:enable Metrics/AbcSize
194
131
  # rubocop:enable Metrics/MethodLength
195
- end
196
-
197
- ##
198
- # Zips the temporary files (collection.anki21, collection.anki2, and media) into a new *.apkg package file.
199
- #
200
- # The temporary files, and the temporary directory they were in, are deleted after zipping.
201
- def zip
202
- create_zip_file && destroy_temporary_directory
203
- end
204
132
 
205
- private
133
+ def destroy_temporary_directory
134
+ FileUtils.rm_rf(tmpdir)
135
+ end
206
136
 
207
137
  def create_zip_file
208
138
  Zip::File.open(target_zip_file, create: true) do |zip_file|
209
- @tmp_files.each do |file_name|
210
- zip_file.add(file_name, File.join(@tmpdir, file_name))
139
+ tmpfiles.each do |file_name|
140
+ zip_file.add(file_name, File.join(tmpdir, file_name))
211
141
  end
212
142
  end
213
143
  true
214
144
  end
215
145
 
216
- def target_zip_file
217
- "#{@target_directory}/#{@name}.apkg"
146
+ # rubocop:disable Metrics/MethodLength
147
+ def replace_zip_file
148
+ File.rename(target_zip_file, tmp_original_zip_file)
149
+ begin
150
+ create_zip_file
151
+ FileUtils.rm(tmp_original_zip_file)
152
+ rescue StandardError => e
153
+ puts e.backtrace.reverse
154
+ puts e
155
+ puts "An error occurred during zipping the new version of the Anki package."
156
+ puts "The original package has not been changed"
157
+ File.rename(tmp_original_zip_file, target_zip_file)
158
+ end
159
+ true
218
160
  end
161
+ # rubocop:enable Metrics/MethodLength
219
162
 
220
- def destroy_temporary_directory
221
- FileUtils.rm_rf(@tmpdir)
163
+ def target_zip_file
164
+ "#{target_directory}/#{name}.apkg"
222
165
  end
223
166
 
224
- public
225
-
226
- # :nodoc:
227
- def open?
228
- !closed?
229
- end
230
-
231
- # :nodoc:
232
- def closed?
233
- @anki21_database.closed?
234
- end
167
+ def tmp_original_zip_file
168
+ "#{target_zip_file}-old"
169
+ end
235
170
  end
236
171
  end
237
- # rubocop:enable Metrics/ClassLength
@@ -6,39 +6,38 @@ require_relative "card_attributes"
6
6
 
7
7
  module AnkiRecord
8
8
  ##
9
- # Card represents an Anki card.
9
+ # Card represents an Anki card. The cards are indirectly created when creating notes.
10
10
  class Card
11
11
  include CardAttributes
12
12
  include Helpers::TimeHelper
13
13
  include Helpers::SharedConstantsHelper
14
14
 
15
- def initialize(note:, card_template: nil, card_data: nil) # :nodoc:
15
+ # :nodoc:
16
+
17
+ def initialize(note:, card_template: nil, card_data: nil)
16
18
  @note = note
17
19
  if card_template
18
- setup_instance_variables_for_new_card(card_template: card_template)
20
+ setup_instance_variables_for_new_card(card_template:)
19
21
  elsif card_data
20
- setup_instance_variables_from_existing(card_data: card_data)
22
+ setup_instance_variables_from_existing(card_data:)
21
23
  else
22
24
  raise ArgumentError
23
25
  end
24
26
  end
25
27
 
28
+ def save(note_exists_already: false)
29
+ note_exists_already ? update_card_in_collection_anki21 : insert_new_card_in_collection_anki21
30
+ end
31
+
26
32
  private
27
33
 
34
+ # rubocop:disable Metrics/MethodLength
28
35
  def setup_instance_variables_for_new_card(card_template:)
29
36
  raise ArgumentError unless @note.note_type == card_template.note_type
30
37
 
31
- setup_collaborator_object_instance_variables_for_new_card(card_template: card_template)
32
- setup_simple_instance_variables_for_new_card
33
- end
34
-
35
- def setup_collaborator_object_instance_variables_for_new_card(card_template:)
36
38
  @card_template = card_template
37
39
  @deck = @note.deck
38
- @collection = @deck.collection
39
- end
40
-
41
- def setup_simple_instance_variables_for_new_card
40
+ @anki21_database = @deck.anki21_database
42
41
  @id = milliseconds_since_epoch
43
42
  @last_modified_timestamp = seconds_since_epoch
44
43
  @usn = NEW_OBJECT_USN
@@ -47,37 +46,22 @@ module AnkiRecord
47
46
  end
48
47
  @data = "{}"
49
48
  end
49
+ # rubocop:enable Metrics/MethodLength
50
50
 
51
51
  def setup_instance_variables_from_existing(card_data:)
52
- setup_collaborator_object_instance_variables_from_existing(card_data: card_data)
53
- setup_simple_instance_variables_from_existing(card_data: card_data)
54
- end
55
-
56
- def setup_collaborator_object_instance_variables_from_existing(card_data:)
57
- @collection = note.note_type.collection
58
- @deck = collection.find_deck_by id: card_data["did"]
52
+ @anki21_database = note.anki21_database
53
+ @deck = anki21_database.find_deck_by id: card_data["did"]
59
54
  @card_template = note.note_type.card_templates.find do |card_template|
60
55
  card_template.ordinal_number == card_data["ord"]
61
56
  end
62
- end
63
-
64
- def setup_simple_instance_variables_from_existing(card_data:)
65
57
  @last_modified_timestamp = card_data["mod"]
66
58
  %w[id usn type queue due ivl factor reps lapses left odue odid flags data].each do |instance_variable_name|
67
59
  instance_variable_set "@#{instance_variable_name}", card_data[instance_variable_name]
68
60
  end
69
61
  end
70
62
 
71
- public
72
-
73
- def save(note_exists_already: false) # :nodoc:
74
- note_exists_already ? update_card_in_collection_anki21 : insert_new_card_in_collection_anki21
75
- end
76
-
77
- private
78
-
79
63
  def update_card_in_collection_anki21
80
- statement = @collection.anki_package.prepare <<~SQL
64
+ statement = anki21_database.prepare <<~SQL
81
65
  update cards set nid = ?, did = ?, ord = ?, mod = ?, usn = ?, type = ?,
82
66
  queue = ?, due = ?, ivl = ?, factor = ?, reps = ?, lapses = ?,
83
67
  left = ?, odue = ?, odid = ?, flags = ?, data = ? where id = ?
@@ -89,7 +73,7 @@ module AnkiRecord
89
73
  end
90
74
 
91
75
  def insert_new_card_in_collection_anki21
92
- statement = @collection.anki_package.prepare <<~SQL
76
+ statement = anki21_database.prepare <<~SQL
93
77
  insert into cards (id, nid, did, ord,
94
78
  mod, usn, type, queue,
95
79
  due, ivl, factor, reps,
@@ -1,39 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnkiRecord
4
- ##
5
- # Module with the Card class's attribute readers, writers, and accessors.
6
- module CardAttributes
7
- ##
8
- # The card's note object.
9
- attr_reader :note
10
-
11
- ##
12
- # The card's deck object.
13
- attr_reader :deck
14
-
15
- ##
16
- # The card's collection object.
17
- attr_reader :collection
18
-
19
- ##
20
- # The card's card template object.
21
- attr_reader :card_template
22
-
23
- ##
24
- # The card's id.
25
- #
26
- # This is also the number of milliseconds since the 1970 epoch at which the card was created.
27
- attr_reader :id
28
-
29
- ##
30
- # The number of seconds since the 1970 epoch at which the card was last modified.
31
- attr_reader :last_modified_timestamp
32
-
33
- ##
34
- # The card's update sequence number.
35
- attr_reader :usn
36
-
37
- attr_reader :type, :queue, :due, :ivl, :factor, :reps, :lapses, :left, :odue, :odid, :flags, :data
4
+ module CardAttributes # :nodoc:
5
+ attr_reader :anki21_database, :note, :deck, :card_template, :id, :last_modified_timestamp, :usn, :type, :queue,
6
+ :due, :ivl, :factor, :reps, :lapses, :left, :odue, :odid, :flags, :data
38
7
  end
39
8
  end
@@ -14,9 +14,9 @@ module AnkiRecord
14
14
 
15
15
  @note_type = note_type
16
16
  if args
17
- setup_card_template_instance_variables_from_existing(args: args)
17
+ setup_card_template_instance_variables_from_existing(args:)
18
18
  else
19
- setup_card_template_instance_variables(name: name)
19
+ setup_card_template_instance_variables(name:)
20
20
  end
21
21
 
22
22
  @note_type.add_card_template self
@@ -5,25 +5,25 @@ module AnkiRecord
5
5
  # Module with the CardTemplate class's attribute readers, writers, and accessors.
6
6
  module CardTemplateAttributes
7
7
  ##
8
- # The card template's name.
8
+ # The card template's name
9
9
  attr_accessor :name
10
10
 
11
11
  ##
12
- # The card template's font style in the browser.
12
+ # The card template's font style in the browser
13
13
  attr_accessor :browser_font_style
14
14
 
15
15
  ##
16
- # The card template's font size used in the browser.
16
+ # The card template's font size used in the browser
17
17
  attr_accessor :browser_font_size
18
18
 
19
19
  ##
20
- # The card template's question format.
20
+ # The card template's question format
21
21
  attr_reader :question_format
22
22
 
23
23
  ##
24
- # Sets the question format of the card template.
24
+ # Sets the question format of the card template
25
25
  #
26
- # Raises an ArgumentError if the specified format attempts to use invalid fields.
26
+ # Raises an ArgumentError if the specified format attempts to use invalid fields
27
27
  def question_format=(format)
28
28
  fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
29
29
  capture.chomp("}}").reverse.chomp("{{").reverse
@@ -38,13 +38,13 @@ module AnkiRecord
38
38
  end
39
39
 
40
40
  ##
41
- # The card template's answer format.
41
+ # The card template's answer format
42
42
  attr_reader :answer_format
43
43
 
44
44
  ##
45
- # Sets the answer format of the card template.
45
+ # Sets the answer format of the card template
46
46
  #
47
- # Raises an ArgumentError if the specified format attempts to use invalid fields.
47
+ # Raises an ArgumentError if the specified format attempts to use invalid fields
48
48
  def answer_format=(format)
49
49
  fields_in_specified_format = format.scan(/{{.+?}}/).map do |capture|
50
50
  capture.chomp("}}").reverse.chomp("{{").reverse
@@ -59,11 +59,11 @@ module AnkiRecord
59
59
  end
60
60
 
61
61
  ##
62
- # The card template's note type object.
62
+ # The card template's note type object
63
63
  attr_reader :note_type
64
64
 
65
65
  ##
66
- # 0 for the first card template of the note type, 1 for the second, etc.
66
+ # 0 for the first card template of the note type, 1 for the second, etc
67
67
  attr_reader :ordinal_number
68
68
  end
69
69
  end