anki-importer 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.project ADDED
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>anki-import</name>
4
+ <comment></comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ </buildSpec>
9
+ <natures>
10
+ <nature>org.radrails.rails.core.railsnature</nature>
11
+ </natures>
12
+ </projectDescription>
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+ gem 'activesupport', '>= 3.0.0', :require => ['active_support/core_ext']
3
+ gem 'sqlite3-ruby', '>= 1.3.1', :require => 'sqlite3'
4
+
5
+ group :development do
6
+ gem "rspec", "~> 2.0.0"
7
+ gem "bundler", "~> 1.0.0"
8
+ gem "jeweler", "~> 1.5.0.pre5"
9
+ gem "rcov", ">= 0"
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (3.0.1)
5
+ diff-lcs (1.1.2)
6
+ git (1.2.5)
7
+ jeweler (1.5.0.pre5)
8
+ bundler (~> 1.0.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ rake (0.8.7)
12
+ rcov (0.9.9)
13
+ rspec (2.0.0)
14
+ rspec-core (= 2.0.0)
15
+ rspec-expectations (= 2.0.0)
16
+ rspec-mocks (= 2.0.0)
17
+ rspec-core (2.0.0)
18
+ rspec-expectations (2.0.0)
19
+ diff-lcs (>= 1.1.2)
20
+ rspec-mocks (2.0.0)
21
+ rspec-core (= 2.0.0)
22
+ rspec-expectations (= 2.0.0)
23
+ sqlite3-ruby (1.3.1)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ activesupport (>= 3.0.0)
30
+ bundler (~> 1.0.0)
31
+ jeweler (~> 1.5.0.pre5)
32
+ rcov
33
+ rspec (~> 2.0.0)
34
+ sqlite3-ruby (>= 1.3.1)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Victor Costan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = anki_importer
2
+
3
+ Imports data from Anki deck databases.
4
+
5
+ == Contributing to anki_importer
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Victor Costan. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "anki-importer"
16
+ gem.summary = %Q{Importer for Anki decks}
17
+ gem.description = %Q{Extracts models, facts and cards from Anki deck databases.}
18
+ gem.email = "victor@costan.us"
19
+ gem.homepage = "http://github.com/pwnall/anki_importer"
20
+ gem.authors = ["Victor Costan"]
21
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
22
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
23
+ # spec.add_runtime_dependency 'jabber4r', '> 0.1'
24
+ # spec.add_development_dependency 'rspec', '> 1.2.3'
25
+ gem.add_runtime_dependency 'sqlite3-ruby', '>= 1.3.1'
26
+ gem.add_runtime_dependency 'activesupport', '>= 3.0.0'
27
+ gem.add_development_dependency "rspec", "~> 2.0.0"
28
+ gem.add_development_dependency "bundler", "~> 1.0.0"
29
+ gem.add_development_dependency "jeweler", "~> 1.5.0.pre5"
30
+ gem.add_development_dependency "rcov", ">= 0"
31
+ end
32
+ Jeweler::RubygemsDotOrgTasks.new
33
+
34
+ require 'rspec/core'
35
+ require 'rspec/core/rake_task'
36
+ RSpec::Core::RakeTask.new(:spec) do |spec|
37
+ spec.pattern = FileList['spec/**/*_spec.rb']
38
+ end
39
+
40
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
41
+ spec.pattern = 'spec/**/*_spec.rb'
42
+ spec.rcov = true
43
+ end
44
+
45
+ task :default => :spec
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "anki-importer #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,95 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{anki-importer}
8
+ s.version = "0.1.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Victor Costan"]
12
+ s.date = %q{2010-10-26}
13
+ s.description = %q{Extracts models, facts and cards from Anki deck databases.}
14
+ s.email = %q{victor@costan.us}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".project",
22
+ ".rspec",
23
+ "Gemfile",
24
+ "Gemfile.lock",
25
+ "LICENSE.txt",
26
+ "README.rdoc",
27
+ "Rakefile",
28
+ "VERSION",
29
+ "anki-importer.gemspec",
30
+ "lib/anki/importer.rb",
31
+ "lib/anki/importer/card_model.rb",
32
+ "lib/anki/importer/deck.rb",
33
+ "lib/anki/importer/fact.rb",
34
+ "lib/anki/importer/field.rb",
35
+ "lib/anki/importer/model.rb",
36
+ "spec/anki_importer_spec.rb",
37
+ "spec/fixtures/deck.anki",
38
+ "spec/spec_helper.rb"
39
+ ]
40
+ s.homepage = %q{http://github.com/pwnall/anki_importer}
41
+ s.require_paths = ["lib"]
42
+ s.rubygems_version = %q{1.3.7}
43
+ s.summary = %q{Importer for Anki decks}
44
+ s.test_files = [
45
+ "spec/anki_importer_spec.rb",
46
+ "spec/spec_helper.rb"
47
+ ]
48
+
49
+ if s.respond_to? :specification_version then
50
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
51
+ s.specification_version = 3
52
+
53
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
54
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0"])
55
+ s.add_runtime_dependency(%q<sqlite3-ruby>, [">= 1.3.1"])
56
+ s.add_development_dependency(%q<rspec>, ["~> 2.0.0"])
57
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
58
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.0.pre5"])
59
+ s.add_development_dependency(%q<rcov>, [">= 0"])
60
+ s.add_runtime_dependency(%q<sqlite3-ruby>, [">= 1.3.1"])
61
+ s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0"])
62
+ s.add_development_dependency(%q<rspec>, ["~> 2.0.0"])
63
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
64
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.0.pre5"])
65
+ s.add_development_dependency(%q<rcov>, [">= 0"])
66
+ else
67
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
68
+ s.add_dependency(%q<sqlite3-ruby>, [">= 1.3.1"])
69
+ s.add_dependency(%q<rspec>, ["~> 2.0.0"])
70
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
71
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre5"])
72
+ s.add_dependency(%q<rcov>, [">= 0"])
73
+ s.add_dependency(%q<sqlite3-ruby>, [">= 1.3.1"])
74
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
75
+ s.add_dependency(%q<rspec>, ["~> 2.0.0"])
76
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
77
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre5"])
78
+ s.add_dependency(%q<rcov>, [">= 0"])
79
+ end
80
+ else
81
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
82
+ s.add_dependency(%q<sqlite3-ruby>, [">= 1.3.1"])
83
+ s.add_dependency(%q<rspec>, ["~> 2.0.0"])
84
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
85
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre5"])
86
+ s.add_dependency(%q<rcov>, [">= 0"])
87
+ s.add_dependency(%q<sqlite3-ruby>, [">= 1.3.1"])
88
+ s.add_dependency(%q<activesupport>, [">= 3.0.0"])
89
+ s.add_dependency(%q<rspec>, ["~> 2.0.0"])
90
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
91
+ s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre5"])
92
+ s.add_dependency(%q<rcov>, [">= 0"])
93
+ end
94
+ end
95
+
@@ -0,0 +1,98 @@
1
+ # :nodoc: namespace
2
+ module Anki
3
+ # :nodoc: namespace
4
+ module Importer
5
+
6
+ # Schema for Anki cards.
7
+ #
8
+ # Each model has multiple card models, and card is generated by exactly one
9
+ # model, from one fact.
10
+ class CardModel
11
+ # Name assigned in the Anki UI.
12
+ attr_reader :name
13
+ # Generally empty.
14
+ attr_reader :description
15
+
16
+ # True if the model is cards are generated based on this model.
17
+ attr_reader :active
18
+
19
+ # Shown during the query stage and maybe during the answer stage.
20
+ #
21
+ # %(field_name)s inlines field values.
22
+ attr_reader :question_template
23
+
24
+ # Shown during the answer stage.
25
+ #
26
+ # %(field_name)s inlines field values. This isn't necessarily related what the
27
+ # user's answer is verified against.
28
+ attr_reader :answer_template
29
+
30
+ # Hash with the following keys: :font_family, :font_size, :color, :text_align.
31
+ attr_reader :question_style
32
+
33
+ # Hash with the following keys: :font_family, :font_size, :color, :text_align.
34
+ attr_reader :answer_style
35
+
36
+ # If true, the question is showed in the answer stage.
37
+ #
38
+ # Otherwise, the answer presumably contains the question.
39
+ attr_reader :question_in_answer
40
+
41
+ # The field that the user's answer is checked against.
42
+ #
43
+ # If this is true, the user is asked to type an answer in the query stage, and
44
+ # their answer is checked against the field value here.
45
+ attr_reader :answer_field
46
+
47
+ # Unique ID in the fields table.
48
+ attr_reader :anki_id
49
+ # The model that this field belongs to.
50
+ attr_reader :model
51
+
52
+ # Reads the card models from an Anki deck.
53
+ #
54
+ # Args:
55
+ # deck_db:: a Sqlite3::Datbase
56
+ # deck: the (under construction) Anki::Importer::Deck for deck_db
57
+ #
58
+ # Returns an array of Field instances.
59
+ def self.from_db(deck_db, deck)
60
+ query = 'SELECT id, modelId, name, description, active, qformat, aformat, questionInAnswer, questionFontFamily, questionFontSize, questionFontColour, questionAlign, answerFontFamily, answerFontSize, answerFontColour, answerAlign, typeAnswer FROM cardModels ORDER BY ordinal'
61
+ models = deck_db.execute(query).map do |anki_id, model_id, name,
62
+ description, active, question_template, answer_template,
63
+ question_in_answer, question_font_family, question_font_size,
64
+ question_font_color, question_align, answer_font_family,
65
+ answer_font_size, answer_font_color, answer_align, answer_field_name|
66
+
67
+ model = deck.models_by_id[model_id]
68
+ question_style = { :font_family => question_font_family,
69
+ :font_size => question_font_size, :color => question_font_color,
70
+ :text_align => question_align }
71
+ answer_style = { :font_family => answer_font_family,
72
+ :font_size => answer_font_size, :color => answer_font_color,
73
+ :text_align => answer_align }
74
+ answer_field = model.fields.find { |f| f.name == answer_field_name }
75
+
76
+ self.new anki_id, model, name, description, active == 1,
77
+ question_template, answer_template, question_style, answer_style,
78
+ answer_field
79
+ end
80
+ end
81
+
82
+ # :nodoc: private
83
+ def initialize(anki_id, model, name, description, active, question_template,
84
+ answer_template, question_style, answer_style, answer_field)
85
+ @anki_id = anki_id
86
+ @model = model
87
+ @name = name
88
+ @description = description
89
+ @active = active
90
+ @question_template = question_template
91
+ @answer_template = answer_template
92
+ @question_style = question_style
93
+ @answer_style = answer_style
94
+ @answer_field = answer_field
95
+ end
96
+ end # class Anki::Importer::CardModel
97
+ end # namespace Anki::Importer
98
+ end # namespace Anki
@@ -0,0 +1,88 @@
1
+ # :nodoc: namespace
2
+ module Anki
3
+ # :nodoc: namespace
4
+ module Importer
5
+
6
+ # The root of the Anki object graph.
7
+ class Deck
8
+ # All the facts (associations) in the deck.
9
+ attr_reader :facts
10
+ # All the models in the deck.
11
+ attr_reader :models
12
+ # All the fields (fact schema elements) in the deck.
13
+ attr_reader :fields
14
+ # All the card models (schemas for cards) in the deck.
15
+ attr_reader :card_models
16
+
17
+ # Reads an Anki deck database.
18
+ #
19
+ # Args:
20
+ # deck_path:: path to an Anki deck on the filesystem
21
+ #
22
+ # Returns a deck.
23
+ def self.from_file(deck_path)
24
+ deck_db = SQLite3::Database.new deck_path
25
+ from_db deck_db
26
+ end
27
+
28
+ # Reads an Anki deck database.
29
+ #
30
+ # Args:
31
+ # deck_db:: a Sqlite3::Datbase
32
+ #
33
+ # Returns a deck.
34
+ def self.from_db(deck_db)
35
+ deck = self.new
36
+ deck.models = Model.from_db deck_db
37
+ deck.fields = Field.from_db deck_db, deck
38
+ deck.fields.each { |field| field.model.add_field field }
39
+ deck.card_models = CardModel.from_db deck_db, deck
40
+ deck.card_models.each { |cmodel| cmodel.model.add_card_model cmodel }
41
+ deck.facts = Fact.from_db deck_db, deck
42
+ deck.facts.each { |fact| fact.model.add_fact fact }
43
+
44
+ deck
45
+ end
46
+
47
+ # :nodoc: private
48
+ def initialize
49
+ @models_by_id = {}
50
+ @fields_by_id = {}
51
+ @card_models_by_id = {}
52
+ @facts_by_id = {}
53
+ end
54
+
55
+ # :nodoc: private
56
+ def models=(new_models)
57
+ @models = new_models
58
+ @models_by_id = new_models.index_by(&:anki_id)
59
+ end
60
+ # :nodoc: private
61
+ attr_reader :models_by_id
62
+
63
+ # :nodoc: private
64
+ def fields=(new_fields)
65
+ @fields = new_fields
66
+ @fields_by_id = new_fields.index_by(&:anki_id)
67
+ end
68
+ # :nodoc: private
69
+ attr_reader :fields_by_id
70
+
71
+ # :nodoc: private
72
+ def card_models=(new_card_models)
73
+ @card_models = new_card_models
74
+ @card_models_by_id = new_card_models.index_by(&:anki_id)
75
+ end
76
+ # :nodoc: private
77
+ attr_reader :card_models_by_id
78
+
79
+ # :nodoc: private
80
+ def facts=(new_facts)
81
+ @facts = new_facts
82
+ @facts_by_id = new_facts.index_by(&:anki_id)
83
+ end
84
+ # :nodoc: private
85
+ attr_reader :facts_by_id
86
+ end # class Anki::Importer::Deck
87
+ end # namespace Anki::Importer
88
+ end # namespace Anki
@@ -0,0 +1,59 @@
1
+ # :nodoc: namespace
2
+ module Anki
3
+ # :nodoc: namespace
4
+ module Importer
5
+
6
+ # An association tested by flash cards.
7
+ class Fact
8
+ # Assigned in the Anki UI. Space-separated string.
9
+ attr_reader :tags
10
+ # Fact creation time.
11
+ attr_reader :created_at
12
+ # Fact modification time.
13
+ attr_reader :modified_at
14
+
15
+ # Maps Fields with their values.
16
+ attr_reader :fields
17
+
18
+ # Unique ID in the facts table.
19
+ attr_reader :anki_id
20
+ # The model that this field belongs to.
21
+ attr_reader :model
22
+
23
+ # Reads the models from an Anki deck.
24
+ #
25
+ # Args:
26
+ # deck_db:: a Sqlite3::Datbase
27
+ # deck: the (under construction) Anki::Importer::Deck for deck_db
28
+ #
29
+ # Returns an array of Field instances.
30
+ def self.from_db(deck_db, deck)
31
+ fact_query = 'SELECT id, modelId, tags, created, modified FROM facts'
32
+ facts = deck_db.execute(fact_query).map do |anki_id, model_id, tags,
33
+ t_created, t_modified|
34
+ self.new anki_id, deck.models_by_id[model_id], tags, Time.at(t_created),
35
+ Time.at(t_modified)
36
+ end
37
+ facts_by_id = facts.index_by(&:anki_id)
38
+
39
+ field = 'SELECT factId, fieldModelId, value FROM fields'
40
+ deck_db.execute(field) do |fact_id, field_model_id, value|
41
+ fact = facts_by_id[fact_id]
42
+ field = deck.fields_by_id[field_model_id]
43
+ fact.fields[field] = value
44
+ end
45
+ facts
46
+ end
47
+
48
+ # :nodoc: private
49
+ def initialize(anki_id, model, tags, created_at, modified_at)
50
+ @anki_id = anki_id
51
+ @model = model
52
+ @tags = tags
53
+ @created_at = created_at
54
+ @modified_at = modified_at
55
+ @fields = {}
56
+ end
57
+ end # class Anki::Importer::Field
58
+ end # namespace Anki::Importer
59
+ end # namespace Anki
@@ -0,0 +1,57 @@
1
+ # :nodoc: namespace
2
+ module Anki
3
+ # :nodoc: namespace
4
+ module Importer
5
+
6
+ # Schema element for Anki facts.
7
+ #
8
+ # Each model has multiple fields. Each fact obeys the schema set by fields.
9
+ class Field
10
+ # Name assigned in the Anki UI.
11
+ attr_reader :name
12
+ # Generally empty.
13
+ attr_reader :description
14
+
15
+ # True if no empty field values are allowed.
16
+ attr_reader :required
17
+ # True if all facts must have unique values for the field.
18
+ attr_reader :unique
19
+ # True if the field is a sort key (number to sort the cards by).
20
+ attr_reader :numeric
21
+
22
+ # Unique ID in the fields table.
23
+ attr_reader :anki_id
24
+ # The model that this field belongs to.
25
+ attr_reader :model
26
+
27
+ # Reads the fields from an Anki deck.
28
+ #
29
+ # Args:
30
+ # deck_db:: a Sqlite3::Datbase
31
+ # deck: the (under construction) Anki::Importer::Deck for deck_db
32
+ #
33
+ # Returns an array of Field instances.
34
+ def self.from_db(deck_db, deck)
35
+ query = 'SELECT id, modelId, name, description, features, required, "unique", numeric FROM fieldModels ORDER BY ordinal'
36
+ models = deck_db.execute(query).map do |anki_id, model_id, name,
37
+ description, features, required, unique, numeric|
38
+ self.new anki_id, deck.models_by_id[model_id], name, description,
39
+ features, required == 1, unique == 1, numeric == 1
40
+ end
41
+ end
42
+
43
+ # :nodoc: private
44
+ def initialize(anki_id, model, name, description, features, required, unique,
45
+ numeric)
46
+ @anki_id = anki_id
47
+ @model = model
48
+ @name = name
49
+ @description = description
50
+ @features = features
51
+ @required = required
52
+ @unique = unique
53
+ @numeric = numeric
54
+ end
55
+ end # class Anki::Importer::Field
56
+ end # namespace Anki::Importer
57
+ end # namespace Anki
@@ -0,0 +1,92 @@
1
+ # :nodoc: namespace
2
+ module Anki
3
+ # :nodoc: namespace
4
+ module Importer
5
+
6
+ # Schema for Anki cards.
7
+ #
8
+ # A deck can have multiple models.
9
+ class Model
10
+ # Name assigned in the Anki UI.
11
+ attr_reader :name
12
+ # Generally empty.
13
+ attr_reader :description
14
+ # Assigned in the Anki UI. Space-separated string.
15
+ attr_reader :tags
16
+ # Unique ID in the models table.
17
+ attr_reader :anki_id
18
+
19
+ # Facts implementing this model.
20
+ attr_reader :facts
21
+
22
+ # Fields belonging to this model.
23
+ attr_reader :fields
24
+
25
+ # Schema for cards based off this model.
26
+ attr_reader :card_models
27
+
28
+ # Reads the models from an Anki deck.
29
+ #
30
+ # Args:
31
+ # deck_db:: a Sqlite3::Datbase
32
+ #
33
+ # Returns an array of Model instances.
34
+ def self.from_db(deck_db)
35
+ query = 'SELECT id, name, description, tags, features FROM models'
36
+ models = deck_db.execute(query).map do |anki_id, name, description, tags,
37
+ features|
38
+ self.new anki_id, name, description, tags, features
39
+ end
40
+ end
41
+
42
+ # :nodoc: private
43
+ def initialize(anki_id, name, description, tags, features)
44
+ @anki_id = anki_id
45
+ @name = name
46
+ @description = description
47
+ @tags = tags
48
+ @features = features
49
+
50
+ @fields = []
51
+ @fields_by_id = {}
52
+ @card_models = []
53
+ @card_models_by_id = {}
54
+ @facts = []
55
+ @facts_by_id = {}
56
+ end
57
+
58
+ # :nodoc: private
59
+ def add_field(field)
60
+ if @fields_by_id[field.anki_id]
61
+ raise ArgumentError, 'The Field has a duplicate Anki ID.'
62
+ end
63
+ @fields << field
64
+ @fields_by_id[field.anki_id] = field
65
+ end
66
+ # :nodoc: private
67
+ attr_reader :fields_by_id
68
+
69
+ # :nodoc: private
70
+ def add_card_model(card_model)
71
+ if @card_models_by_id[card_model.anki_id]
72
+ raise ArgumentError, 'The CardModel has a duplicate Anki ID.'
73
+ end
74
+ @card_models << card_model
75
+ @card_models_by_id[card_model.anki_id] = card_model
76
+ end
77
+ # :nodoc: private
78
+ attr_reader :card_models_by_id
79
+
80
+ # :nodoc: private
81
+ def add_fact(fact)
82
+ if @facts_by_id[fact.anki_id]
83
+ raise ArgumentError, 'The Fact has a duplicate Anki ID.'
84
+ end
85
+ @facts << fact
86
+ @facts_by_id[fact.anki_id] = fact
87
+ end
88
+ # :nodoc: private
89
+ attr_reader :facts_by_id
90
+ end # class Anki::Importer::Model
91
+ end # namespace Anki::Importer
92
+ end # namespace Anki
@@ -0,0 +1,11 @@
1
+ # NOTE: can't do granular requires because active_support crashes
2
+ # require 'active_support/core_ext/enumerable'
3
+ # require 'active_support/core_ext/string'
4
+ require 'active_support/core_ext'
5
+ require 'sqlite3'
6
+
7
+ require 'anki/importer/card_model.rb'
8
+ require 'anki/importer/deck.rb'
9
+ require 'anki/importer/fact.rb'
10
+ require 'anki/importer/field.rb'
11
+ require 'anki/importer/model.rb'
@@ -0,0 +1,152 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "anki-importer" do
4
+ let(:deck) do
5
+ deck_path = File.join File.dirname(__FILE__), 'fixtures', 'deck.anki'
6
+ Anki::Importer::Deck.from_file deck_path
7
+ end
8
+
9
+ describe 'Deck' do
10
+ it 'should have a Model' do
11
+ deck.should have(1).model
12
+ end
13
+
14
+ it 'should have Fields' do
15
+ deck.should have(3).fields
16
+ end
17
+
18
+ it 'should have CardModels' do
19
+ deck.should have(3).card_models
20
+ end
21
+ end
22
+
23
+ let(:model) { deck.models.first }
24
+
25
+ describe 'Model' do
26
+ it 'should extract the name correctly' do
27
+ model.name.should == 'CSS Color'
28
+ end
29
+
30
+ it 'should extract tags correctly' do
31
+ model.tags.should == 'css html color'
32
+ end
33
+
34
+ it 'should be associated with fields' do
35
+ model.should have(3).fields
36
+ end
37
+
38
+ it 'should be associated with card models' do
39
+ model.should have(3).card_models
40
+ end
41
+
42
+ it 'should be associated with facts' do
43
+ model.should have(5).facts
44
+ end
45
+ end
46
+
47
+ let(:name_field) { deck.fields.find { |f| f.name == 'Color name' } }
48
+ let(:code_field) { deck.fields.find { |f| f.name == 'HTML color code' } }
49
+ let(:rgb_field) { deck.fields.find { |f| f.name == 'RGB color code' } }
50
+
51
+ describe 'Field' do
52
+ it 'should decode names correctly' do
53
+ name_field.should_not be_nil
54
+ code_field.should_not be_nil
55
+ rgb_field.should_not be_nil
56
+ end
57
+
58
+ it 'should be associated with the right model' do
59
+ name_field.model.should == model
60
+ end
61
+
62
+ it 'should decode required correctly' do
63
+ name_field.required.should be_true
64
+ rgb_field.required.should be_false
65
+ end
66
+
67
+ it 'should decode unique correctly' do
68
+ name_field.unique.should be_false
69
+ code_field.unique.should be_true
70
+ end
71
+
72
+ it 'should decode numeric correctly' do
73
+ name_field.numeric.should be_false
74
+ end
75
+
76
+ it 'should order fields correctly' do
77
+ model.fields.should == [name_field, code_field, rgb_field]
78
+ end
79
+ end
80
+
81
+ let(:front_cm) { deck.card_models.find { |cm| cm.name == 'Name to code' } }
82
+ let(:back_cm) { deck.card_models.find { |cm| cm.name == 'Code to name' } }
83
+ let(:disabled_cm) do
84
+ deck.card_models.find { |cm| cm.name == 'Disabled card' }
85
+ end
86
+ describe 'CardModel' do
87
+ it 'should decode names correctly' do
88
+ front_cm.should_not be_nil
89
+ back_cm.should_not be_nil
90
+ disabled_cm.should_not be_nil
91
+ end
92
+
93
+ it 'should be associated with the right model' do
94
+ front_cm.model.should == model
95
+ end
96
+
97
+ it 'should decode active' do
98
+ front_cm.active.should be_true
99
+ disabled_cm.active.should be_false
100
+ end
101
+
102
+ it 'should decode the question template' do
103
+ front_cm.question_template.should == '%(Color name)s'
104
+ end
105
+
106
+ it 'should decode the answer template' do
107
+ front_cm.answer_template.should == '%(HTML color code)s'
108
+ end
109
+
110
+ it 'should decode question formatting' do
111
+ golden = { :font_family => 'Arial', :font_size => 20,
112
+ :color => '#000000', :text_align => 0 }
113
+ front_cm.question_style.should == golden
114
+ end
115
+
116
+ it 'should decode answer formatting' do
117
+ golden = { :font_family => 'Arial', :font_size => 20,
118
+ :color => '#000000', :text_align => 0 }
119
+ front_cm.answer_style.should == golden
120
+ end
121
+
122
+ it 'should associate the answer field correctly' do
123
+ front_cm.answer_field.should == code_field
124
+ back_cm.answer_field.should be_nil
125
+ end
126
+
127
+ it 'should order models correctly' do
128
+ model.card_models.should == [front_cm, back_cm, disabled_cm]
129
+ end
130
+ end
131
+
132
+ let(:white_fact) do
133
+ deck.facts.find { |fact| fact.fields[name_field] == 'white' }
134
+ end
135
+ let(:black_fact) do
136
+ black_hanzi = [233, 187, 145].pack('C*') # 黑
137
+ deck.facts.find { |fact| fact.fields[name_field] == black_hanzi }
138
+ end
139
+ describe 'Fact' do
140
+ it 'should decode field values correctly' do
141
+ white_fact.should_not be_nil
142
+ end
143
+
144
+ it 'should decode unicode field values correctly' do
145
+ black_fact.should_not be_nil
146
+ end
147
+
148
+ it 'should be associated with the right model' do
149
+ white_fact.model.should == model
150
+ end
151
+ end
152
+ end
Binary file
@@ -0,0 +1,10 @@
1
+ require 'anki/importer'
2
+ require 'rspec'
3
+
4
+ # Requires supporting files with custom matchers and macros, etc,
5
+ # in ./support/ and its subdirectories.
6
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
7
+
8
+ RSpec.configure do |config|
9
+
10
+ end
metadata ADDED
@@ -0,0 +1,276 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anki-importer
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - Victor Costan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-26 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ prerelease: false
23
+ name: activesupport
24
+ version_requirements: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
35
+ requirement: *id001
36
+ type: :runtime
37
+ - !ruby/object:Gem::Dependency
38
+ prerelease: false
39
+ name: sqlite3-ruby
40
+ version_requirements: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 25
46
+ segments:
47
+ - 1
48
+ - 3
49
+ - 1
50
+ version: 1.3.1
51
+ requirement: *id002
52
+ type: :runtime
53
+ - !ruby/object:Gem::Dependency
54
+ prerelease: false
55
+ name: rspec
56
+ version_requirements: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ hash: 15
62
+ segments:
63
+ - 2
64
+ - 0
65
+ - 0
66
+ version: 2.0.0
67
+ requirement: *id003
68
+ type: :development
69
+ - !ruby/object:Gem::Dependency
70
+ prerelease: false
71
+ name: bundler
72
+ version_requirements: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ hash: 23
78
+ segments:
79
+ - 1
80
+ - 0
81
+ - 0
82
+ version: 1.0.0
83
+ requirement: *id004
84
+ type: :development
85
+ - !ruby/object:Gem::Dependency
86
+ prerelease: false
87
+ name: jeweler
88
+ version_requirements: &id005 !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ hash: -1876988220
94
+ segments:
95
+ - 1
96
+ - 5
97
+ - 0
98
+ - pre5
99
+ version: 1.5.0.pre5
100
+ requirement: *id005
101
+ type: :development
102
+ - !ruby/object:Gem::Dependency
103
+ prerelease: false
104
+ name: rcov
105
+ version_requirements: &id006 !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ hash: 3
111
+ segments:
112
+ - 0
113
+ version: "0"
114
+ requirement: *id006
115
+ type: :development
116
+ - !ruby/object:Gem::Dependency
117
+ prerelease: false
118
+ name: sqlite3-ruby
119
+ version_requirements: &id007 !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ hash: 25
125
+ segments:
126
+ - 1
127
+ - 3
128
+ - 1
129
+ version: 1.3.1
130
+ requirement: *id007
131
+ type: :runtime
132
+ - !ruby/object:Gem::Dependency
133
+ prerelease: false
134
+ name: activesupport
135
+ version_requirements: &id008 !ruby/object:Gem::Requirement
136
+ none: false
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ hash: 7
141
+ segments:
142
+ - 3
143
+ - 0
144
+ - 0
145
+ version: 3.0.0
146
+ requirement: *id008
147
+ type: :runtime
148
+ - !ruby/object:Gem::Dependency
149
+ prerelease: false
150
+ name: rspec
151
+ version_requirements: &id009 !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ~>
155
+ - !ruby/object:Gem::Version
156
+ hash: 15
157
+ segments:
158
+ - 2
159
+ - 0
160
+ - 0
161
+ version: 2.0.0
162
+ requirement: *id009
163
+ type: :development
164
+ - !ruby/object:Gem::Dependency
165
+ prerelease: false
166
+ name: bundler
167
+ version_requirements: &id010 !ruby/object:Gem::Requirement
168
+ none: false
169
+ requirements:
170
+ - - ~>
171
+ - !ruby/object:Gem::Version
172
+ hash: 23
173
+ segments:
174
+ - 1
175
+ - 0
176
+ - 0
177
+ version: 1.0.0
178
+ requirement: *id010
179
+ type: :development
180
+ - !ruby/object:Gem::Dependency
181
+ prerelease: false
182
+ name: jeweler
183
+ version_requirements: &id011 !ruby/object:Gem::Requirement
184
+ none: false
185
+ requirements:
186
+ - - ~>
187
+ - !ruby/object:Gem::Version
188
+ hash: -1876988220
189
+ segments:
190
+ - 1
191
+ - 5
192
+ - 0
193
+ - pre5
194
+ version: 1.5.0.pre5
195
+ requirement: *id011
196
+ type: :development
197
+ - !ruby/object:Gem::Dependency
198
+ prerelease: false
199
+ name: rcov
200
+ version_requirements: &id012 !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ hash: 3
206
+ segments:
207
+ - 0
208
+ version: "0"
209
+ requirement: *id012
210
+ type: :development
211
+ description: Extracts models, facts and cards from Anki deck databases.
212
+ email: victor@costan.us
213
+ executables: []
214
+
215
+ extensions: []
216
+
217
+ extra_rdoc_files:
218
+ - LICENSE.txt
219
+ - README.rdoc
220
+ files:
221
+ - .document
222
+ - .project
223
+ - .rspec
224
+ - Gemfile
225
+ - Gemfile.lock
226
+ - LICENSE.txt
227
+ - README.rdoc
228
+ - Rakefile
229
+ - VERSION
230
+ - anki-importer.gemspec
231
+ - lib/anki/importer.rb
232
+ - lib/anki/importer/card_model.rb
233
+ - lib/anki/importer/deck.rb
234
+ - lib/anki/importer/fact.rb
235
+ - lib/anki/importer/field.rb
236
+ - lib/anki/importer/model.rb
237
+ - spec/anki_importer_spec.rb
238
+ - spec/fixtures/deck.anki
239
+ - spec/spec_helper.rb
240
+ has_rdoc: true
241
+ homepage: http://github.com/pwnall/anki_importer
242
+ licenses: []
243
+
244
+ post_install_message:
245
+ rdoc_options: []
246
+
247
+ require_paths:
248
+ - lib
249
+ required_ruby_version: !ruby/object:Gem::Requirement
250
+ none: false
251
+ requirements:
252
+ - - ">="
253
+ - !ruby/object:Gem::Version
254
+ hash: 3
255
+ segments:
256
+ - 0
257
+ version: "0"
258
+ required_rubygems_version: !ruby/object:Gem::Requirement
259
+ none: false
260
+ requirements:
261
+ - - ">="
262
+ - !ruby/object:Gem::Version
263
+ hash: 3
264
+ segments:
265
+ - 0
266
+ version: "0"
267
+ requirements: []
268
+
269
+ rubyforge_project:
270
+ rubygems_version: 1.3.7
271
+ signing_key:
272
+ specification_version: 3
273
+ summary: Importer for Anki decks
274
+ test_files:
275
+ - spec/anki_importer_spec.rb
276
+ - spec/spec_helper.rb