daigaku 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +7 -4
  3. data/CODE_OF_CONDUCT.md +77 -0
  4. data/README.md +11 -11
  5. data/bin/daigaku +6 -2
  6. data/daigaku.gemspec +22 -26
  7. data/lib/daigaku.rb +0 -1
  8. data/lib/daigaku/chapter.rb +3 -4
  9. data/lib/daigaku/coloring.rb +22 -27
  10. data/lib/daigaku/configuration.rb +25 -28
  11. data/lib/daigaku/congratulator.rb +17 -5
  12. data/lib/daigaku/course.rb +9 -8
  13. data/lib/daigaku/exceptions.rb +0 -2
  14. data/lib/daigaku/generator.rb +13 -15
  15. data/lib/daigaku/github_client.rb +5 -5
  16. data/lib/daigaku/loadable.rb +23 -14
  17. data/lib/daigaku/loading/chapters.rb +0 -2
  18. data/lib/daigaku/loading/courses.rb +0 -2
  19. data/lib/daigaku/loading/units.rb +0 -2
  20. data/lib/daigaku/markdown.rb +1 -0
  21. data/lib/daigaku/markdown/printer.rb +89 -0
  22. data/lib/daigaku/markdown/ruby_doc.rb +15 -15
  23. data/lib/daigaku/solution.rb +23 -15
  24. data/lib/daigaku/storeable.rb +11 -12
  25. data/lib/daigaku/task.rb +1 -1
  26. data/lib/daigaku/terminal.rb +3 -4
  27. data/lib/daigaku/terminal/cli.rb +8 -8
  28. data/lib/daigaku/terminal/courses.rb +22 -19
  29. data/lib/daigaku/terminal/output.rb +46 -53
  30. data/lib/daigaku/terminal/setup.rb +13 -18
  31. data/lib/daigaku/terminal/solutions.rb +27 -32
  32. data/lib/daigaku/terminal/welcome.rb +9 -11
  33. data/lib/daigaku/test.rb +7 -10
  34. data/lib/daigaku/test_result.rb +54 -21
  35. data/lib/daigaku/unit.rb +2 -4
  36. data/lib/daigaku/version.rb +1 -1
  37. data/lib/daigaku/views.rb +29 -33
  38. data/lib/daigaku/views/chapters_menu.rb +16 -20
  39. data/lib/daigaku/views/courses_menu.rb +12 -15
  40. data/lib/daigaku/views/main_menu.rb +23 -23
  41. data/lib/daigaku/views/menu.rb +14 -18
  42. data/lib/daigaku/views/splash.rb +11 -13
  43. data/lib/daigaku/views/subscriber.rb +38 -0
  44. data/lib/daigaku/views/task_view.rb +97 -80
  45. data/lib/daigaku/views/top_bar.rb +4 -10
  46. data/lib/daigaku/views/units_menu.rb +16 -21
  47. data/lib/daigaku/window.rb +12 -70
  48. data/spec/daigaku/chapter_spec.rb +23 -18
  49. data/spec/daigaku/coloring_spec.rb +0 -1
  50. data/spec/daigaku/configuration_spec.rb +54 -50
  51. data/spec/daigaku/congratulator_spec.rb +11 -8
  52. data/spec/daigaku/course_spec.rb +75 -52
  53. data/spec/daigaku/generator_spec.rb +24 -25
  54. data/spec/daigaku/github_client_spec.rb +17 -18
  55. data/spec/daigaku/loading/chapters_spec.rb +2 -3
  56. data/spec/daigaku/loading/courses_spec.rb +2 -3
  57. data/spec/daigaku/loading/units_spec.rb +4 -5
  58. data/spec/daigaku/markdown/ruby_doc_spec.rb +12 -6
  59. data/spec/daigaku/reference_solution_spec.rb +8 -10
  60. data/spec/daigaku/solution_spec.rb +21 -22
  61. data/spec/daigaku/storeable_spec.rb +12 -10
  62. data/spec/daigaku/task_spec.rb +3 -4
  63. data/spec/daigaku/terminal/cli_spec.rb +29 -21
  64. data/spec/daigaku/terminal/courses_spec.rb +104 -99
  65. data/spec/daigaku/terminal/output_spec.rb +44 -39
  66. data/spec/daigaku/terminal/setup_spec.rb +1 -3
  67. data/spec/daigaku/terminal/solutions_spec.rb +0 -2
  68. data/spec/daigaku/terminal/welcome_spec.rb +0 -2
  69. data/spec/daigaku/terminal_spec.rb +5 -7
  70. data/spec/daigaku/test_example_spec.rb +16 -14
  71. data/spec/daigaku/test_result_spec.rb +21 -25
  72. data/spec/daigaku/test_spec.rb +11 -12
  73. data/spec/daigaku/unit_spec.rb +24 -27
  74. data/spec/daigaku/views/chapters_menu_spec.rb +0 -1
  75. data/spec/daigaku/views/courses_menu_spec.rb +0 -1
  76. data/spec/daigaku/views/menu_spec.rb +1 -2
  77. data/spec/daigaku/views/task_view_spec.rb +0 -2
  78. data/spec/daigaku/views/units_menu_spec.rb +0 -1
  79. data/spec/daigaku/views_spec.rb +0 -1
  80. data/spec/daigaku_spec.rb +9 -12
  81. data/spec/path_helpers_spec.rb +11 -12
  82. data/spec/resource_helpers_spec.rb +11 -12
  83. data/spec/spec_helper.rb +3 -4
  84. data/spec/support/macros/content_helpers.rb +16 -17
  85. data/spec/support/macros/mock_helpers.rb +6 -6
  86. data/spec/support/macros/path_helpers.rb +15 -15
  87. data/spec/support/macros/resource_helpers.rb +34 -35
  88. metadata +32 -44
@@ -1,13 +1,25 @@
1
1
  module Daigaku
2
-
3
2
  class Congratulator
4
-
5
3
  def self.message
6
- lines = Terminal.text(:congratulations).lines.map(&:strip).compact
7
- count = lines.count.zero? ? 0 : (lines.count - 1)
8
- random_value = rand(0..count)
4
+ new.message
5
+ end
6
+
7
+ def message
9
8
  lines[random_value]
10
9
  end
11
10
 
11
+ private
12
+
13
+ def lines
14
+ @lines ||= Terminal.text(:congratulations).lines.map(&:strip).compact
15
+ end
16
+
17
+ def random_value
18
+ rand(0..lines_count)
19
+ end
20
+
21
+ def lines_count
22
+ [lines.count - 1, 0].max
23
+ end
12
24
  end
13
25
  end
@@ -2,12 +2,11 @@ require 'fileutils'
2
2
 
3
3
  module Daigaku
4
4
  class Course
5
-
6
5
  attr_reader :title, :path, :author, :link
7
6
 
8
7
  def initialize(path)
9
- @path = path
10
- @title = File.basename(path).gsub(/\_+/, ' ')
8
+ @path = path
9
+ @title = File.basename(path).gsub(/\_+/, ' ')
11
10
  @author = QuickStore.store.get(key(:author))
12
11
  end
13
12
 
@@ -16,11 +15,11 @@ module Daigaku
16
15
  end
17
16
 
18
17
  def started?
19
- chapters.reduce(false) { |started, chapter| started ||= chapter.started? }
18
+ chapters.any?(&:started?)
20
19
  end
21
20
 
22
21
  def mastered?
23
- chapters.reduce(true) { |mastered, chapter| mastered &&= chapter.mastered? }
22
+ chapters.all?(&:mastered?)
24
23
  end
25
24
 
26
25
  def key(key_name)
@@ -44,7 +43,6 @@ module Daigaku
44
43
 
45
44
  Zip::File.open(file_path) do |zip_file|
46
45
  zip_file.each do |entry|
47
-
48
46
  if options[:github_repo]
49
47
  first, *others = entry.to_s.split('/')
50
48
  directory = File.join(first.split('-')[0..-2].join('-'), others)
@@ -66,10 +64,13 @@ module Daigaku
66
64
  end
67
65
 
68
66
  FileUtils.rm(file_path)
69
- rescue Exception => e
67
+ rescue StandardError => e
70
68
  puts e
71
69
  old_dir = "#{course_dir}_old/"
72
- FileUtils.copy_entry(old_dir, "#{course_dir}/", true) if Dir.exist?(old_dir)
70
+
71
+ if Dir.exist?(old_dir)
72
+ FileUtils.copy_entry(old_dir, "#{course_dir}/", true)
73
+ end
73
74
  ensure
74
75
  old_dir = "#{course_dir}_old/"
75
76
  FileUtils.rm_r(old_dir) if Dir.exist?(old_dir)
@@ -1,5 +1,4 @@
1
1
  module Daigaku
2
-
3
2
  class Error < StandardError; end
4
3
  class CourseNotFoundError < Error; end
5
4
  class ChaptersNotFoundError < Error; end
@@ -15,5 +14,4 @@ module Daigaku
15
14
  class NoUrlError < Error; end
16
15
  class NoZipFileUrlError < Error; end
17
16
  end
18
-
19
17
  end
@@ -1,21 +1,21 @@
1
- module Daigaku
2
- require 'fileutils'
3
- require 'active_support'
4
- require 'active_support/core_ext'
1
+ require 'fileutils'
5
2
 
3
+ module Daigaku
6
4
  class Generator
5
+ LEADING_NUMBERS = /^\d+[\_\-\s]/
6
+ PART_JOINTS = /[\_\-\s]+/
7
7
 
8
8
  def scaffold(courses_path, target_path)
9
- Dir[File.join(courses_path, "*/*/*/*.md")].each do |file|
9
+ Dir[File.join(courses_path, '*/*/*/*.md')].each do |file|
10
10
  content_dir_parts = file.split('/')[-4..-2].map do |part|
11
11
  clean_up_path_part(part)
12
12
  end
13
13
 
14
14
  content_dir = File.join(content_dir_parts)
15
- directory = File.join(target_path, File.dirname(content_dir))
15
+ directory = File.join(target_path, File.dirname(content_dir))
16
16
 
17
17
  solution_file = File.basename(content_dir) + Solution::FILE_SUFFIX
18
- file_path = File.join(directory, solution_file)
18
+ file_path = File.join(directory, solution_file)
19
19
 
20
20
  create_dir(directory)
21
21
  create_file(file_path)
@@ -25,9 +25,9 @@ module Daigaku
25
25
  def prepare
26
26
  begin
27
27
  solutions_path = Daigaku.config.solutions_path
28
- rescue ConfigurationError => e
29
- base_dir = File.dirname(Daigaku.config.courses_path)
30
- solutions_dir = Daigaku::Configuration::SOLUTIONS_DIR
28
+ rescue ConfigurationError
29
+ base_dir = File.dirname(Daigaku.config.courses_path)
30
+ solutions_dir = Daigaku::Configuration::SOLUTIONS_DIR
31
31
  solutions_path = File.join(base_dir, solutions_dir)
32
32
  end
33
33
 
@@ -41,20 +41,18 @@ module Daigaku
41
41
  private
42
42
 
43
43
  def create_dir(path)
44
- return if path.blank?
44
+ return if path.nil? || path.empty?
45
45
  FileUtils.makedirs(path) unless Dir.exist?(path)
46
46
  end
47
47
 
48
48
  def create_file(path)
49
- return if path.blank?
49
+ return if path.nil? || path.empty?
50
50
  create_dir(File.dirname(path))
51
51
  FileUtils.touch(path) unless File.exist?(path)
52
52
  end
53
53
 
54
54
  def clean_up_path_part(text)
55
- leading_numbers = /^\d+[\_\-\s]/
56
- part_joints = /[\_\-\s]+/
57
- text.gsub(leading_numbers, '').gsub(part_joints, '_').downcase
55
+ text.gsub(LEADING_NUMBERS, '').gsub(PART_JOINTS, '_').downcase
58
56
  end
59
57
  end
60
58
  end
@@ -4,7 +4,6 @@ require 'date'
4
4
 
5
5
  module Daigaku
6
6
  module GithubClient
7
-
8
7
  # Returns the url to the master zip file of the Github repo.
9
8
  def self.master_zip_url(user_and_repo)
10
9
  "https://github.com/#{user_and_repo}/archive/master.zip"
@@ -13,7 +12,7 @@ module Daigaku
13
12
  # Returns the timestamp of updated_at for the repo from the Github API.
14
13
  def self.updated_at(user_and_repo)
15
14
  url = "https://api.github.com/repos/#{user_and_repo}"
16
- JSON.parse(open(url).read)['updated_at']
15
+ JSON.parse(URI.open(url).read)['updated_at']
17
16
  end
18
17
 
19
18
  # Returns whether the pushed_at time from Github API is newer than the
@@ -21,9 +20,10 @@ module Daigaku
21
20
  def self.updated?(user_and_repo)
22
21
  return false unless user_and_repo
23
22
 
24
- course = Course.new(user_and_repo.split('/').last)
25
- stored_time = QuickStore.store.get(course.key(:updated_at))
26
- current_time = self.updated_at(user_and_repo)
23
+ course = Course.new(user_and_repo.split('/').last)
24
+ stored_time = QuickStore.store.get(course.key(:updated_at))
25
+ current_time = updated_at(user_and_repo)
26
+
27
27
  DateTime.parse(stored_time) < DateTime.parse(current_time)
28
28
  end
29
29
  end
@@ -1,23 +1,32 @@
1
1
  module Daigaku
2
2
  module Loadable
3
-
4
- require 'active_support/inflector'
5
-
6
3
  def load(path)
7
- if Dir.exist?(path)
8
- dirs = Dir.entries(path).select do |entry|
9
- !entry.match(/\./)
10
- end
4
+ return [] unless Dir.exist?(path)
11
5
 
12
- dirs.sort.map do |dir|
13
- dir_path = File.join(path, dir)
14
- class_name = self.to_s.demodulize.singularize
15
- "Daigaku::#{class_name}".constantize.new(dir_path)
16
- end
17
- else
18
- Array.new
6
+ dirs = Dir.entries(path).select do |entry|
7
+ !entry.match(/\./)
19
8
  end
9
+
10
+ dirs.sort.map do |dir|
11
+ dir_path = File.join(path, dir)
12
+ module_name = demodulize(to_s)
13
+ class_name = singularize(module_name)
14
+ daigaku_class(class_name).new(dir_path)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def demodulize(string)
21
+ string.split('::').last
20
22
  end
21
23
 
24
+ def singularize(string)
25
+ string.end_with?('s') ? string[0..-2] : string
26
+ end
27
+
28
+ def daigaku_class(name)
29
+ Kernel.const_get("Daigaku::#{name}")
30
+ end
22
31
  end
23
32
  end
@@ -1,9 +1,7 @@
1
1
  module Daigaku
2
2
  module Loading
3
-
4
3
  class Chapters
5
4
  extend Daigaku::Loadable
6
5
  end
7
-
8
6
  end
9
7
  end
@@ -1,9 +1,7 @@
1
1
  module Daigaku
2
2
  module Loading
3
-
4
3
  class Courses
5
4
  extend Daigaku::Loadable
6
5
  end
7
-
8
6
  end
9
7
  end
@@ -1,9 +1,7 @@
1
1
  module Daigaku
2
2
  module Loading
3
-
4
3
  class Units
5
4
  extend Daigaku::Loadable
6
5
  end
7
-
8
6
  end
9
7
  end
@@ -1 +1,2 @@
1
1
  require 'daigaku/markdown/ruby_doc'
2
+ require 'daigaku/markdown/printer'
@@ -0,0 +1,89 @@
1
+ require 'curses'
2
+ require_relative 'ruby_doc'
3
+
4
+ module Daigaku
5
+ module Markdown
6
+ class Printer
7
+ H1 = /\A\#{1}[^#]+/ # '# heading'
8
+ H2 = /\A\#{2}[^#]+/ # '## sub heading'
9
+ BOLD = /\*[^*]*\*/ # '*text*'
10
+ LINE = /\A-{3,}/ # '---' vertical line
11
+ CODE = /`[^`]*`/ # '`code line`'
12
+
13
+ attr_reader :window
14
+
15
+ def initialize(window:)
16
+ @window = window
17
+ end
18
+
19
+ def print(text)
20
+ text_line = RubyDoc.parse(text)
21
+
22
+ case text_line
23
+ when H1 then print_h1(text_line)
24
+ when H2 then print_h2(text_line)
25
+ when /(#{CODE}|#{BOLD})/ then print_code_or_bold(text_line)
26
+ when BOLD then print_bold(text_line)
27
+ when LINE then print_line
28
+ else print_escaped(text_line)
29
+ end
30
+ end
31
+
32
+ def print_h1(text)
33
+ window.heading(text.sub(/\A#\s?/, ''))
34
+ end
35
+
36
+ def print_h2(text)
37
+ text_decoration = Curses::A_UNDERLINE | Curses::A_NORMAL
38
+ window.emphasize(text.sub(/\A##\s?/, ''), text_decoration)
39
+ end
40
+
41
+ def print_code_or_bold(text)
42
+ emphasized = false
43
+ highlighted = false
44
+
45
+ text.chars.each_with_index do |char, index|
46
+ if char == '*' && text[index - 1] != '\\'
47
+ emphasized = !emphasized
48
+ next
49
+ end
50
+
51
+ if char == '`'
52
+ highlighted = !highlighted
53
+ next
54
+ end
55
+
56
+ character = text[index..(index + 1)] == '\\*' ? '' : char
57
+
58
+ if highlighted
59
+ window.red(character)
60
+ elsif emphasized
61
+ window.emphasize(character)
62
+ else
63
+ window.write(character)
64
+ end
65
+ end
66
+ end
67
+
68
+ def print_bold(text)
69
+ text.chars.each_with_index do |char, index|
70
+ if char == '*' && text[index - 1] != '\\'
71
+ emphasized = !emphasized
72
+ next
73
+ end
74
+
75
+ character = text[index..(index + 1)].to_s == '\\*' ? '' : char
76
+ emphasized ? window.emphasize(character) : window.write(character)
77
+ end
78
+ end
79
+
80
+ def print_line
81
+ window.write('-' * (Curses.cols - 2))
82
+ end
83
+
84
+ def print_escaped(text)
85
+ window.write(text.gsub(/(\\#)/, '#'))
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,15 +1,14 @@
1
1
  require 'cgi'
2
2
 
3
3
  module Daigaku
4
- class Markdown
4
+ module Markdown
5
5
  class RubyDoc
6
-
7
- RUBY_DOC_URL = "http://ruby-doc.org".freeze
6
+ RUBY_DOC_URL = 'http://ruby-doc.org'.freeze
8
7
  CORE_BASE_URL = "#{RUBY_DOC_URL}/core-#{RUBY_VERSION}".freeze
9
8
  STDLIB_BASE_URL = "#{RUBY_DOC_URL}/stdlib-#{RUBY_VERSION}".freeze
10
9
 
11
- CORE_REGEX = /\(ruby-doc core:\s?(.*)\)/.freeze
12
- STDLIB_REGEX = /\(ruby-doc stdlib:\s?(.*)\)/.freeze
10
+ CORE_REGEX = /\(ruby-doc core:\s?(.*)\)/
11
+ STDLIB_REGEX = /\(ruby-doc stdlib:\s?(.*)\)/
13
12
 
14
13
  class << self
15
14
  def parse(text)
@@ -43,19 +42,19 @@ module Daigaku
43
42
  end
44
43
 
45
44
  def doc_regex(type, capture)
46
- /\(ruby-doc #{type}: #{capture}\)/
45
+ Regexp.new(Regexp.escape("(ruby-doc #{type}: #{capture})"))
47
46
  end
48
47
 
49
48
  def core_link(text)
50
49
  constants = ruby_constants(text).join('/')
51
- method = ruby_method(text)
50
+ method = ruby_method(text)
52
51
 
53
52
  "#{CORE_BASE_URL}/#{constants}.html#{method}"
54
53
  end
55
54
 
56
55
  def stdlib_link(text)
57
- constants = ruby_constants(text).join('/')
58
- method = ruby_method(text)
56
+ constants = ruby_constants(text).join('/')
57
+ method = ruby_method(text)
59
58
  libdoc_part = "libdoc/#{ruby_stdlib(text)}/rdoc"
60
59
 
61
60
  "#{STDLIB_BASE_URL}/#{libdoc_part}/#{constants}.html#{method}"
@@ -68,7 +67,7 @@ module Daigaku
68
67
  # Else the lib is created from the constants, e.g.
69
68
  # (ruby-doc stdlib: Time) => 'time'
70
69
  def ruby_stdlib(text)
71
- parts = text.split(' ')
70
+ parts = text.split
72
71
 
73
72
  if parts.length > 1
74
73
  parts.first.strip.downcase
@@ -78,7 +77,7 @@ module Daigaku
78
77
  end
79
78
 
80
79
  def ruby_constants(text)
81
- parts = text.split(' ').last.split(/::|#/)
80
+ parts = text.split.last.split(/::|#/)
82
81
  select_capitalized(parts)
83
82
  end
84
83
 
@@ -86,19 +85,20 @@ module Daigaku
86
85
  method = text.split(/::|#/).last
87
86
  return '' unless downcased?(method)
88
87
 
89
- method_type = text.match(/#/) ? 'i' : 'c'
90
- method_name = CGI.escape(method.strip).gsub('%', '-').gsub(/\A-/, '')
88
+ method_type = text =~ /#/ ? 'i' : 'c'
89
+ method_name = CGI.escape(method.strip).tr('%', '-').gsub(/\A-/, '')
91
90
  "#method-#{method_type}-#{method_name}"
92
91
  end
93
92
 
94
93
  def select_capitalized(parts)
95
- parts.select { |part| part[0] == part[0].upcase }
94
+ parts.select do |part|
95
+ part[0].match(/\w/) && part[0] == part[0].upcase
96
+ end
96
97
  end
97
98
 
98
99
  def downcased?(text)
99
100
  text == text.downcase
100
101
  end
101
-
102
102
  end
103
103
  end
104
104
  end