anki_record 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +31 -9
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +4 -2
  6. data/README.md +114 -31
  7. data/anki_record.gemspec +1 -5
  8. data/lib/anki_record/anki_package/anki_package.rb +245 -0
  9. data/lib/anki_record/anki_package/database_setup_constants.rb +91 -0
  10. data/lib/anki_record/card/card.rb +108 -0
  11. data/lib/anki_record/card/card_attributes.rb +39 -0
  12. data/lib/anki_record/card_template/card_template.rb +64 -0
  13. data/lib/anki_record/card_template/card_template_attributes.rb +69 -0
  14. data/lib/anki_record/collection/collection.rb +180 -0
  15. data/lib/anki_record/collection/collection_attributes.rb +35 -0
  16. data/lib/anki_record/deck/deck.rb +101 -0
  17. data/lib/anki_record/deck/deck_attributes.rb +30 -0
  18. data/lib/anki_record/deck/deck_defaults.rb +19 -0
  19. data/lib/anki_record/{deck_options_group.rb → deck_options_group/deck_options_group.rb} +10 -29
  20. data/lib/anki_record/deck_options_group/deck_options_group_attributes.rb +23 -0
  21. data/lib/anki_record/helpers/checksum_helper.rb +2 -5
  22. data/lib/anki_record/helpers/data_query_helper.rb +13 -0
  23. data/lib/anki_record/helpers/shared_constants_helper.rb +1 -3
  24. data/lib/anki_record/helpers/time_helper.rb +7 -5
  25. data/lib/anki_record/note/note.rb +181 -0
  26. data/lib/anki_record/note/note_attributes.rb +56 -0
  27. data/lib/anki_record/note_field/note_field.rb +62 -0
  28. data/lib/anki_record/note_field/note_field_attributes.rb +39 -0
  29. data/lib/anki_record/note_field/note_field_defaults.rb +19 -0
  30. data/lib/anki_record/note_type/note_type.rb +161 -0
  31. data/lib/anki_record/note_type/note_type_attributes.rb +80 -0
  32. data/lib/anki_record/note_type/note_type_defaults.rb +38 -0
  33. data/lib/anki_record/version.rb +1 -1
  34. data/lib/anki_record.rb +1 -16
  35. metadata +26 -16
  36. data/lib/anki_record/anki_package.rb +0 -194
  37. data/lib/anki_record/card.rb +0 -75
  38. data/lib/anki_record/card_template.rb +0 -105
  39. data/lib/anki_record/collection.rb +0 -105
  40. data/lib/anki_record/db/anki_schema_definition.rb +0 -77
  41. data/lib/anki_record/db/clean_collection21_record.rb +0 -10
  42. data/lib/anki_record/db/clean_collection2_record.rb +0 -10
  43. data/lib/anki_record/deck.rb +0 -101
  44. data/lib/anki_record/note.rb +0 -135
  45. data/lib/anki_record/note_field.rb +0 -84
  46. data/lib/anki_record/note_type.rb +0 -233
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "deck_attributes"
4
+ require_relative "deck_defaults"
5
+ require_relative "../helpers/shared_constants_helper"
6
+ require_relative "../helpers/time_helper"
7
+
8
+ module AnkiRecord
9
+ ##
10
+ # Deck represents an Anki deck.
11
+ # In the collection.anki21 database, the deck is a JSON object
12
+ # which is part of a larger JSON object: the value of the col record's decks column.
13
+ class Deck
14
+ include DeckAttributes
15
+ include DeckDefaults
16
+ include SharedConstantsHelper
17
+ include TimeHelper
18
+
19
+ ##
20
+ # Instantiates a new Deck object belonging to +collection+ with name +name+.
21
+ def initialize(collection:, name: nil, args: nil)
22
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
23
+
24
+ @collection = collection
25
+ if args
26
+ setup_deck_instance_variables_from_existing(args: args)
27
+ else
28
+ setup_deck_instance_variables(name: name)
29
+ end
30
+
31
+ @collection.add_deck self
32
+ save
33
+ end
34
+
35
+ ##
36
+ # Saves the deck (or updates it) in the collection.anki21 database.
37
+ def save
38
+ collection_decks_hash = collection.decks_json
39
+ collection_decks_hash[@id] = to_h
40
+ sql = "update col set decks = ? where id = ?"
41
+ collection.anki_package.prepare(sql).execute([JSON.generate(collection_decks_hash), collection.id])
42
+ end
43
+
44
+ def to_h # :nodoc:
45
+ {
46
+ id: @id, mod: @last_modified_timestamp, name: @name, usn: @usn,
47
+ lrnToday: @learn_today, revToday: @review_today, newToday: @new_today, timeToday: @time_today,
48
+ collapsed: @collapsed_in_main_window, browserCollapsed: @collapsed_in_browser,
49
+ desc: @description, dyn: @dyn, conf: @deck_options_group.id,
50
+ extendNew: @extend_new, extendRev: @extend_review
51
+ }
52
+ end
53
+
54
+ # :nodoc:
55
+ # :nocov:
56
+ def inspect
57
+ "#<AnkiRecord::Deck:#{object_id} id: #{id} name: #{name} description: #{description}>"
58
+ end
59
+
60
+ private
61
+
62
+ # rubocop:disable Metrics/MethodLength
63
+ # rubocop:disable Metrics/AbcSize
64
+ def setup_deck_instance_variables_from_existing(args:)
65
+ @id = args["id"]
66
+ @last_modified_timestamp = args["mod"]
67
+ @name = args["name"]
68
+ @usn = args["usn"]
69
+ @learn_today = args["lrnToday"]
70
+ @review_today = args["revToday"]
71
+ @new_today = args["newToday"]
72
+ @time_today = args["timeToday"]
73
+ @collapsed_in_main_window = args["collapsed"]
74
+ @collapsed_in_browser = args["browserCollapsed"]
75
+ @description = args["desc"]
76
+ @dyn = args["dyn"]
77
+ @deck_options_group = @collection.find_deck_options_group_by id: args["conf"]
78
+ @extend_new = args["extendNew"]
79
+ @extend_review = args["extendRev"]
80
+ end
81
+ # rubocop:enable Metrics/MethodLength
82
+ # rubocop:enable Metrics/AbcSize
83
+
84
+ # rubocop:disable Metrics/MethodLength
85
+ def setup_deck_instance_variables(name:)
86
+ @id = milliseconds_since_epoch
87
+ @last_modified_timestamp = seconds_since_epoch
88
+ @name = name
89
+ @usn = NEW_OBJECT_USN
90
+ @learn_today = @review_today = @new_today = @time_today = default_deck_today_array
91
+ @collapsed_in_main_window = default_collapsed
92
+ @collapsed_in_browser = default_collapsed
93
+ @description = ""
94
+ @dyn = NON_FILTERED_DECK_DYN
95
+ @deck_options_group = @collection.find_deck_options_group_by id: default_deck_options_group_id
96
+ @extend_new = 0
97
+ @extend_review = 0
98
+ end
99
+ # rubocop:enable Metrics/MethodLength
100
+ end
101
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ # Module with the Deck class's attribute readers, writers, and accessors.
5
+ module DeckAttributes
6
+ ##
7
+ # The deck's collection object.
8
+ attr_reader :collection
9
+
10
+ ##
11
+ # The deck's name.
12
+ attr_accessor :name
13
+
14
+ ##
15
+ # The deck's description.
16
+ attr_accessor :description
17
+
18
+ ##
19
+ # The deck's id.
20
+ attr_reader :id
21
+
22
+ ##
23
+ # The number of seconds since the 1970 epoch when the deck was last modified.
24
+ attr_reader :last_modified_timestamp
25
+
26
+ ##
27
+ # The deck's deck options group object.
28
+ attr_reader :deck_options_group
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ module DeckDefaults # :nodoc:
5
+ private
6
+
7
+ def default_deck_options_group_id
8
+ collection.deck_options_groups.min_by(&:id).id
9
+ end
10
+
11
+ def default_deck_today_array
12
+ [0, 0].freeze
13
+ end
14
+
15
+ def default_collapsed
16
+ false
17
+ end
18
+ end
19
+ end
@@ -1,37 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pry"
4
-
5
- require_relative "helpers/shared_constants_helper"
6
- require_relative "helpers/time_helper"
3
+ require_relative "../helpers/shared_constants_helper"
4
+ require_relative "../helpers/time_helper"
5
+ require_relative "deck_options_group_attributes"
7
6
 
8
7
  module AnkiRecord
9
8
  ##
10
- # Represents the set of options that can be applied to a deck
9
+ # DeckOptionsGroup represents a set of options that can be applied to an Anki deck.
11
10
  class DeckOptionsGroup
11
+ include DeckOptionsGroupAttributes
12
12
  include SharedConstantsHelper
13
13
  include TimeHelper
14
14
 
15
15
  ##
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
21
- attr_accessor :name
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
30
-
31
- ##
32
- # Instantiates a new deck options group called +name+ with defaults
16
+ # Instantiates a new deck options group belonging to +collection+ with name +name+.
33
17
  def initialize(collection:, name: nil, args: nil)
34
- # TODO: extract this check to a shared helper
35
18
  raise ArgumentError unless (name && args.nil?) || (args && args["name"])
36
19
 
37
20
  @collection = collection
@@ -41,6 +24,8 @@ module AnkiRecord
41
24
  else
42
25
  setup_deck_options_group_instance_variables(name: name)
43
26
  end
27
+
28
+ @collection.add_deck_options_group self
44
29
  end
45
30
 
46
31
  private
@@ -49,7 +34,7 @@ module AnkiRecord
49
34
  # rubocop:disable Metrics/AbcSize
50
35
  def setup_deck_options_group_instance_variables_from_existing(args:)
51
36
  @id = args["id"]
52
- @last_modified_time = args["mod"]
37
+ @last_modified_timestamp = args["mod"]
53
38
  @name = args["name"]
54
39
  @usn = args["usn"]
55
40
  @max_taken = args["maxTaken"]
@@ -68,14 +53,10 @@ module AnkiRecord
68
53
  @new_gather_priority = args["newGatherPriority"]
69
54
  @bury_interday_learning = args["buryInterdayLearning"]
70
55
  end
71
- # rubocop:enable Metrics/AbcSize
72
- # rubocop:enable Metrics/MethodLength
73
56
 
74
- # rubocop:disable Metrics/MethodLength
75
- # rubocop:disable Metrics/AbcSize
76
57
  def setup_deck_options_group_instance_variables(name:)
77
58
  @id = milliseconds_since_epoch
78
- @last_modified_time = seconds_since_epoch
59
+ @last_modified_timestamp = seconds_since_epoch
79
60
  @name = name
80
61
  @usn = NEW_OBJECT_USN
81
62
  @max_taken = 60
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # Module with the Card class's attribute readers, writers, and accessors.
6
+ module DeckOptionsGroupAttributes
7
+ ##
8
+ # The deck options group's collection object.
9
+ attr_reader :collection
10
+
11
+ ##
12
+ # The deck option group's name.
13
+ attr_accessor :name
14
+
15
+ ##
16
+ # The deck option group's id.
17
+ attr_reader :id
18
+
19
+ ##
20
+ # The number of milliseconds since the 1970 epoch at which the deck options group was modified.
21
+ attr_reader :last_modified_timestamp
22
+ end
23
+ end
@@ -6,13 +6,10 @@ module AnkiRecord
6
6
  ##
7
7
  # A module for the method that calculates the checksum value of notes.
8
8
  #
9
- # This checksum is used by Anki to detect duplicates.
9
+ # This checksum may be used by Anki to detect duplicates.
10
10
  module ChecksumHelper
11
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
12
+ # Returns the integer representation of the first 8 characters of the SHA-1 digest of the +sfld+ argument
16
13
  def checksum(sfld)
17
14
  Digest::SHA1.hexdigest(sfld)[0...8].to_i(16).to_s
18
15
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ module DataQueryHelper # :nodoc:
5
+ def note_cards_data_for_note_id(sql_able:, id:)
6
+ note_data = sql_able.prepare("select * from notes where id = ?").execute([id]).first
7
+ return nil unless note_data
8
+
9
+ cards_data = sql_able.prepare("select * from cards where nid = ?").execute([id]).to_a
10
+ { note_data: note_data, cards_data: cards_data }
11
+ end
12
+ end
13
+ end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnkiRecord
4
- ##
5
- # Helper module to hold the constants used by multiple classes
6
- module SharedConstantsHelper
4
+ module SharedConstantsHelper # :nodoc:
7
5
  NEW_OBJECT_USN = -1
8
6
  NON_FILTERED_DECK_DYN = 0
9
7
  private_constant :NEW_OBJECT_USN, :NON_FILTERED_DECK_DYN
@@ -4,18 +4,20 @@ require "date"
4
4
 
5
5
  module AnkiRecord
6
6
  ##
7
- # Helper module to calculate integer time values since the 1970 epoch
7
+ # Helper module to calculate integer time values since the 1970 epoch.
8
8
  #
9
- # Specifically, the time that has passed since 00:00:00 UTC Jan 1 1970
9
+ # Specifically, the time that has passed since 00:00:00 UTC Jan 1 1970.
10
10
  module TimeHelper
11
11
  ##
12
- # Return the number of milliseconds since the 1970 epoch
12
+ # Returns approximately the number of milliseconds since the 1970 epoch.
13
+ # A random amount of milliseconds between -5000 and 5000 is added so that
14
+ # primary key ids calculated with this should be unique.
13
15
  def milliseconds_since_epoch
14
- DateTime.now.strftime("%Q").to_i
16
+ DateTime.now.strftime("%Q").to_i + rand(-5000..5000)
15
17
  end
16
18
 
17
19
  ##
18
- # Return the number of seconds since the 1970 epoch
20
+ # Returns approximately the number of seconds since the 1970 epoch.
19
21
  def seconds_since_epoch
20
22
  Time.now.to_i
21
23
  end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require_relative "../helpers/checksum_helper"
6
+ require_relative "../helpers/time_helper"
7
+ require_relative "note_attributes"
8
+
9
+ # rubocop:disable Metrics/ClassLength
10
+ module AnkiRecord
11
+ ##
12
+ # Represents an Anki note. The note object corresponds to a record in the `notes`
13
+ # table in the collection.anki21 database.
14
+ class Note
15
+ include ChecksumHelper
16
+ include NoteAttributes
17
+ include TimeHelper
18
+ include SharedConstantsHelper
19
+
20
+ ##
21
+ # Instantiates a note of type +note_type+ and belonging to deck +deck+.
22
+ #
23
+ # If +note_type+ and +deck+ arguments are used, +collection+ and +data should not be given.
24
+ def initialize(note_type: nil, deck: nil, collection: nil, data: nil)
25
+ if note_type && deck
26
+ setup_instance_variables_for_new_note(note_type: note_type, deck: deck)
27
+ elsif collection && data
28
+ setup_instance_variables_from_existing(collection: collection,
29
+ note_data: data[:note_data], cards_data: data[:cards_data])
30
+ else
31
+ raise ArgumentError
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def setup_instance_variables_for_new_note(note_type:, deck:)
38
+ raise ArgumentError unless note_type.collection == deck.collection
39
+
40
+ setup_collaborator_object_instance_variables_for_new_note(note_type: note_type, deck: deck)
41
+ setup_simple_instance_variables_for_new_note
42
+ end
43
+
44
+ def setup_collaborator_object_instance_variables_for_new_note(note_type:, deck:)
45
+ @note_type = note_type
46
+ @deck = deck
47
+ @collection = deck.collection
48
+ @field_contents = setup_empty_field_contents_hash
49
+ @cards = @note_type.card_templates.map do |card_template|
50
+ Card.new(note: self, card_template: card_template)
51
+ end
52
+ end
53
+
54
+ def setup_simple_instance_variables_for_new_note
55
+ @id = milliseconds_since_epoch
56
+ @guid = globally_unique_id
57
+ @last_modified_timestamp = seconds_since_epoch
58
+ @usn = NEW_OBJECT_USN
59
+ @tags = []
60
+ @flags = 0
61
+ @data = ""
62
+ end
63
+
64
+ def setup_instance_variables_from_existing(collection:, note_data:, cards_data:)
65
+ setup_collaborator_object_instance_variables_from_existing(collection: collection, note_data: note_data,
66
+ cards_data: cards_data)
67
+ setup_simple_instance_variables_from_existing(note_data: note_data)
68
+ end
69
+
70
+ def setup_collaborator_object_instance_variables_from_existing(collection:, note_data:, cards_data:)
71
+ @collection = collection
72
+ @note_type = collection.find_note_type_by id: note_data["mid"]
73
+ @field_contents = setup_field_contents_hash_from_existing(note_data: note_data)
74
+ @cards = @note_type.card_templates.map.with_index do |_card_template, index|
75
+ Card.new(note: self, card_data: cards_data[index])
76
+ end
77
+ end
78
+
79
+ def setup_field_contents_hash_from_existing(note_data:)
80
+ field_contents = setup_empty_field_contents_hash
81
+ snake_case_field_names_in_order = note_type.snake_case_field_names
82
+ note_data["flds"].split("\x1F").each_with_index do |fld, ordinal|
83
+ field_contents[snake_case_field_names_in_order[ordinal]] = fld
84
+ end
85
+ field_contents
86
+ end
87
+
88
+ def setup_simple_instance_variables_from_existing(note_data:)
89
+ @id = note_data["id"]
90
+ @guid = note_data["guid"]
91
+ @last_modified_timestamp = note_data["mod"]
92
+ @usn = note_data["usn"]
93
+ @tags = note_data["tags"].split
94
+ @flags = note_data["flags"]
95
+ @data = note_data["data"]
96
+ end
97
+
98
+ def setup_empty_field_contents_hash
99
+ field_contents = {}
100
+ note_type.snake_case_field_names.each { |field_name| field_contents[field_name] = "" }
101
+ field_contents
102
+ end
103
+
104
+ public
105
+
106
+ ##
107
+ # Saves the note to the collection.anki21 database.
108
+ #
109
+ # This also saves the note's cards.
110
+ def save
111
+ collection.find_note_by(id: @id) ? update_note_in_collection_anki21 : insert_new_note_in_collection_anki21
112
+ true
113
+ end
114
+
115
+ private
116
+
117
+ def update_note_in_collection_anki21
118
+ statement = @collection.anki_package.prepare <<~SQL
119
+ update notes set guid = ?, mid = ?, mod = ?, usn = ?, tags = ?,
120
+ flds = ?, sfld = ?, csum = ?, flags = ?, data = ? where id = ?
121
+ SQL
122
+ statement.execute([@guid, note_type.id, @last_modified_timestamp,
123
+ @usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
124
+ checksum(sort_field_value), @flags, @data, @id])
125
+ cards.each { |card| card.save(note_exists_already: true) }
126
+ end
127
+
128
+ def insert_new_note_in_collection_anki21
129
+ statement = @collection.anki_package.prepare <<~SQL
130
+ insert into notes (id, guid, mid, mod, usn, tags, flds, sfld, csum, flags, data)
131
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
132
+ SQL
133
+ statement.execute([@id, @guid, note_type.id, @last_modified_timestamp,
134
+ @usn, @tags.join(" "), field_values_separated_by_us, sort_field_value,
135
+ checksum(sort_field_value), @flags, @data])
136
+ cards.each(&:save)
137
+ end
138
+
139
+ public
140
+
141
+ ##
142
+ # Overrides BasicObject#method_missing and creates "ghost methods".
143
+ #
144
+ # The ghost methods are the setters and getters for the note field values.
145
+ def method_missing(method_name, field_content = nil)
146
+ raise NoMethodError, "##{method_name} is not defined or a ghost method" unless respond_to_missing? method_name
147
+
148
+ method_name = method_name.to_s
149
+ return @field_contents[method_name] unless method_name.end_with?("=")
150
+
151
+ @field_contents[method_name.chomp("=")] = field_content
152
+ end
153
+
154
+ ##
155
+ # This allows #respond_to? to be accurate for the ghost methods created by #method_missing.
156
+ def respond_to_missing?(method_name, *)
157
+ method_name = method_name.to_s
158
+ if method_name.end_with?("=")
159
+ note_type.snake_case_field_names.include?(method_name.chomp("="))
160
+ else
161
+ note_type.snake_case_field_names.include?(method_name)
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def globally_unique_id
168
+ SecureRandom.uuid.slice(5...15)
169
+ end
170
+
171
+ def field_values_separated_by_us
172
+ # The ASCII control code represented by hexadecimal 1F is the Unit Separator (US)
173
+ note_type.snake_case_field_names.map { |field_name| @field_contents[field_name] }.join("\x1F")
174
+ end
175
+
176
+ def sort_field_value
177
+ @field_contents[note_type.snake_case_sort_field_name]
178
+ end
179
+ end
180
+ end
181
+ # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ # Module with the Note class's attribute readers, writers, and accessors.
5
+ module NoteAttributes
6
+ ##
7
+ # The note's id.
8
+ attr_reader :id
9
+
10
+ ##
11
+ # The note's globally unique id.
12
+ attr_reader :guid
13
+
14
+ ##
15
+ # The number of seconds since the 1970 epoch at which the note was last modified.
16
+ attr_reader :last_modified_timestamp
17
+
18
+ ##
19
+ # The note's update sequence number.
20
+ attr_reader :usn
21
+
22
+ ##
23
+ # The note's tags, as an array. The tags are strings.
24
+ attr_reader :tags
25
+
26
+ ##
27
+ # The note's field contents, as a hash.
28
+ attr_reader :field_contents
29
+
30
+ ##
31
+ # The note's deck object.
32
+ #
33
+ # When the note is saved, the cards will belong to this deck.
34
+ attr_reader :deck
35
+
36
+ ##
37
+ # The note's note type object.
38
+ attr_reader :note_type
39
+
40
+ ##
41
+ # The note's collection object.
42
+ attr_reader :collection
43
+
44
+ ##
45
+ # The note's card objects, as an array.
46
+ attr_reader :cards
47
+
48
+ ##
49
+ # Corresponds to the flags column in the collection.anki21 notes table.
50
+ attr_reader :flags
51
+
52
+ ##
53
+ # Corresponds to the data column in the collection.anki21 notes table.
54
+ attr_reader :data
55
+ end
56
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "note_field_attributes"
4
+ require_relative "note_field_defaults"
5
+
6
+ module AnkiRecord
7
+ ##
8
+ # NoteField represents a field of an Anki note type.
9
+ class NoteField
10
+ include NoteFieldAttributes
11
+ include NoteFieldDefaults
12
+
13
+ ##
14
+ # Instantiates a new field for the note type +note_type+ with name +name+.
15
+ def initialize(note_type:, name: nil, args: nil)
16
+ raise ArgumentError unless (name && args.nil?) || (args && args["name"])
17
+
18
+ @note_type = note_type
19
+ if args
20
+ setup_note_field_instance_variables_from_existing(args: args)
21
+ else
22
+ setup_note_field_instance_variables_for_new_field(name: name)
23
+ end
24
+
25
+ @note_type.add_note_field self
26
+ end
27
+
28
+ def to_h # :nodoc:
29
+ {
30
+ name: @name,
31
+ ord: @ordinal_number,
32
+ sticky: @sticky,
33
+ rtl: @right_to_left,
34
+ font: @font_style,
35
+ size: @font_size,
36
+ description: @description
37
+ }
38
+ end
39
+
40
+ private
41
+
42
+ def setup_note_field_instance_variables_from_existing(args:)
43
+ @name = args["name"]
44
+ @ordinal_number = args["ord"]
45
+ @sticky = args["sticky"]
46
+ @right_to_left = args["rtl"]
47
+ @font_style = args["font"]
48
+ @font_size = args["size"]
49
+ @description = args["description"]
50
+ end
51
+
52
+ def setup_note_field_instance_variables_for_new_field(name:)
53
+ @name = name
54
+ @ordinal_number = @note_type.note_fields.length
55
+ @sticky = false
56
+ @right_to_left = false
57
+ @font_style = default_field_font_style
58
+ @font_size = default_field_font_size
59
+ @description = default_field_description
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ ##
5
+ # Module with the NoteField class's attribute readers, writers, and accessors.
6
+ module NoteFieldAttributes
7
+ ##
8
+ # The field's note type.
9
+ attr_reader :note_type
10
+
11
+ ##
12
+ # The field's name.
13
+ attr_accessor :name
14
+
15
+ ##
16
+ # A boolean that indicates if the field is sticky.
17
+ attr_accessor :sticky
18
+
19
+ ##
20
+ # A boolean that indicates if the field is right to left.
21
+ attr_accessor :right_to_left
22
+
23
+ ##
24
+ # The field's font style used when editing.
25
+ attr_accessor :font_style
26
+
27
+ ##
28
+ # The field's font size used when editing.
29
+ attr_accessor :font_size
30
+
31
+ ##
32
+ # The field's description.
33
+ attr_accessor :description
34
+
35
+ ##
36
+ # 0 for the first field of the note type, 1 for the second, etc.
37
+ attr_reader :ordinal_number
38
+ end
39
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiRecord
4
+ module NoteFieldDefaults # :nodoc:
5
+ private
6
+
7
+ def default_field_font_style
8
+ "Arial"
9
+ end
10
+
11
+ def default_field_font_size
12
+ 20
13
+ end
14
+
15
+ def default_field_description
16
+ ""
17
+ end
18
+ end
19
+ end