daigaku 0.2.0 → 1.0.0

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 (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
@@ -2,9 +2,7 @@ require 'daigaku/views/menu'
2
2
 
3
3
  module Daigaku
4
4
  module Views
5
-
6
5
  class CoursesMenu < Menu
7
-
8
6
  private
9
7
 
10
8
  def header_text
@@ -14,17 +12,17 @@ module Daigaku
14
12
  def interact_with(window)
15
13
  while char = window.getch
16
14
  case char
17
- when KEY_UP
18
- @position -= 1
19
- broadcast(:reset_menu_position)
20
- when KEY_DOWN
21
- @position += 1
22
- broadcast(:reset_menu_position)
23
- when 10 # Enter
24
- broadcast(:enter, models[@position])
25
- return
26
- when 27 # ESC
27
- exit
15
+ when KEY_UP
16
+ @position -= 1
17
+ broadcast(:reset_menu_position)
18
+ when KEY_DOWN
19
+ @position += 1
20
+ broadcast(:reset_menu_position)
21
+ when 10 # Enter
22
+ broadcast(:enter, models[@position])
23
+ return
24
+ when 27 # ESC
25
+ exit
28
26
  end
29
27
 
30
28
  @position = items.length - 1 if @position < 0
@@ -41,12 +39,11 @@ module Daigaku
41
39
  non_empty_courses = models.select { |course| !course.chapters.empty? }
42
40
 
43
41
  non_empty_courses.map do |course|
44
- line = "#{course.title}"
42
+ line = course.title
45
43
  self.items_info <<= [(course.author ? "(by #{course.author})" : '')]
46
44
  line
47
45
  end
48
46
  end
49
47
  end
50
-
51
48
  end
52
49
  end
@@ -1,37 +1,37 @@
1
+ require_relative 'subscriber'
2
+
1
3
  module Daigaku
2
4
  module Views
3
-
5
+ # Subscription: `first.subscribe(second)` means
6
+ # first subscribes second on the first's broadcast.
7
+ # second has to have method that is broadcasted.
4
8
  class MainMenu
5
9
  include Views
6
10
 
7
- def initialize
8
- courses_menu = Views::CoursesMenu.new
9
- chapters_menu = Views::ChaptersMenu.new
10
- units_menu = Views::UnitsMenu.new
11
- task_view = Views::TaskView.new
11
+ attr_reader :courses_menu, :chapters_menu, :units_menu, :task_view
12
12
 
13
- # Subscription: `first.subscribe(second)` means
14
- # first subscribes second on the first's broadcast.
15
- # second has to have method that is broadcasted.
13
+ def initialize
14
+ @courses_menu = Views::CoursesMenu.new
15
+ @chapters_menu = Views::ChaptersMenu.new
16
+ @units_menu = Views::UnitsMenu.new
17
+ @task_view = Views::TaskView.new
16
18
 
17
- # top down navigation
18
- courses_menu.subscribe(chapters_menu, on: :enter)
19
- chapters_menu.subscribe(units_menu, on: :enter)
20
- units_menu.subscribe(task_view, on: :enter)
19
+ subscribe_events
20
+ courses_menu.enter
21
+ end
21
22
 
22
- # bottom up navigation
23
- chapters_menu.subscribe(courses_menu, on: :reenter)
24
- units_menu.subscribe(chapters_menu, on: :reenter)
25
- task_view.subscribe(units_menu, on: :reenter)
23
+ private
26
24
 
27
- # position reset
28
- courses_menu.subscribe(chapters_menu, on: :reset_menu_position)
29
- courses_menu.subscribe(units_menu, on: :reset_menu_position)
30
- chapters_menu.subscribe(units_menu, on: :reset_menu_position)
25
+ def subscribe_events
26
+ subscriber = Subscriber.new(
27
+ courses_menu: courses_menu,
28
+ chapters_menu: chapters_menu,
29
+ units_menu: units_menu,
30
+ task_view: task_view
31
+ )
31
32
 
32
- courses_menu.enter
33
+ subscriber.subscribe_events!
33
34
  end
34
35
  end
35
-
36
36
  end
37
37
  end
@@ -2,17 +2,18 @@ require 'wisper'
2
2
 
3
3
  module Daigaku
4
4
  module Views
5
-
6
5
  class Menu
7
6
  include Views
8
7
  include Wisper::Publisher
9
8
 
10
9
  TOP_BAR_TEXT = [
11
- 'Use *UP KEY* and *DOWN KEY* for menu navigation',
12
- 'Enter menu with *RETURN*',
13
- 'Go back with *BACKSPACE*',
14
- 'Exit with *ESC*'
15
- ].join(' | ')
10
+ 'Use * 🠕 * and * 🠗 * for menu navigation',
11
+ 'Enter menu with **',
12
+ 'Go back with **',
13
+ 'Exit with *Esc*'
14
+ ].join(' | ').freeze
15
+
16
+ attr_writer :items_info
16
17
 
17
18
  def initialize
18
19
  @position = 0
@@ -46,44 +47,39 @@ module Daigaku
46
47
  def draw(window, active_index = 0)
47
48
  window.attrset(A_NORMAL)
48
49
  window.setpos(0, 1)
49
- window.print_markdown header_text
50
+ window.print_markdown(header_text)
50
51
 
51
52
  items.each_with_index do |item, index|
52
53
  window.setpos(index + 2, 1)
53
54
  window.print_indicator(models[index])
54
55
  window.attrset(index == active_index ? A_STANDOUT : A_NORMAL)
55
- window.write " #{item.to_s} "
56
+ window.write " #{item} "
56
57
  window.attrset(A_NORMAL)
57
- window.write " #{items_info[index].try(:join, ' ')}"
58
+ window.write " #{items_info[index] && items_info[index].join(' ')}"
58
59
  end
59
60
 
60
61
  window.refresh
61
62
  end
62
63
 
63
64
  def interact_with(window)
64
- raise "Please implement the method #interact_with!"
65
+ raise 'Please implement the method #interact_with!'
65
66
  end
66
67
 
67
68
  def models
68
- raise "Please implement the method #models!"
69
+ raise 'Please implement the method #models!'
69
70
  end
70
71
 
71
72
  def items
72
- raise "Please implement the method #items!"
73
+ raise 'Please implement the method #items!'
73
74
  end
74
75
 
75
76
  def header_text
76
- raise "Please implement the method #header_text!"
77
+ raise 'Please implement the method #header_text!'
77
78
  end
78
79
 
79
80
  def items_info
80
81
  @items_info || []
81
82
  end
82
-
83
- def items_info=(items_info)
84
- @items_info = items_info
85
- end
86
83
  end
87
-
88
84
  end
89
85
  end
@@ -1,14 +1,12 @@
1
1
  module Daigaku
2
2
  module Views
3
-
4
3
  class Splash
5
4
  include Views
6
5
 
7
6
  def initialize
8
- title = "DAIGAKU"
9
- subtitle = "Learning the Ruby programming language dead easy."
10
-
11
- panel = default_window
7
+ title = 'DAIGAKU'
8
+ subtitle = 'Learning the Ruby programming language dead easy.'
9
+ panel = default_window
12
10
 
13
11
  lines.times do |line|
14
12
  panel.setpos(line, 0)
@@ -43,15 +41,15 @@ module Daigaku
43
41
 
44
42
  def ruby_ascii_art
45
43
  [
46
- " ___________ ",
47
- " /.\\ /.\\ /.\\ ",
48
- "/___\\/___\\/___\\",
49
- " \\ \\ . / . / ",
50
- " \\ \\ ./ ./ ",
51
- " \\\\ / / ",
52
- " \\./ "
44
+ ' ___________ ',
45
+ ' /.\\ /.\\ /.\\ ',
46
+ '/___\\/___\\/___\\',
47
+ ' \\ \\ . / . / ',
48
+ ' \\ \\ ./ ./ ',
49
+ ' \\\\ / / ',
50
+ ' \\./ '
53
51
  ]
54
52
  end
55
53
  end
56
54
  end
57
- end
55
+ end
@@ -0,0 +1,38 @@
1
+ module Daigaku
2
+ module Views
3
+ class Subscriber
4
+ attr_reader :courses_menu, :chapters_menu, :units_menu, :task_view
5
+
6
+ def initialize(courses_menu:, chapters_menu:, units_menu:, task_view:)
7
+ @courses_menu = courses_menu
8
+ @chapters_menu = chapters_menu
9
+ @units_menu = units_menu
10
+ @task_view = task_view
11
+ end
12
+
13
+ def subscribe_events!
14
+ subscribe_top_down_navigation
15
+ subscribe_bottom_up_navigation
16
+ subscribe_menu_position_reset
17
+ end
18
+
19
+ def subscribe_top_down_navigation
20
+ courses_menu.subscribe(chapters_menu, on: :enter)
21
+ chapters_menu.subscribe(units_menu, on: :enter)
22
+ units_menu.subscribe(task_view, on: :enter)
23
+ end
24
+
25
+ def subscribe_bottom_up_navigation
26
+ chapters_menu.subscribe(courses_menu, on: :reenter)
27
+ units_menu.subscribe(chapters_menu, on: :reenter)
28
+ task_view.subscribe(units_menu, on: :reenter)
29
+ end
30
+
31
+ def subscribe_menu_position_reset
32
+ courses_menu.subscribe(chapters_menu, on: :reset_menu_position)
33
+ courses_menu.subscribe(units_menu, on: :reset_menu_position)
34
+ chapters_menu.subscribe(units_menu, on: :reset_menu_position)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,41 +1,41 @@
1
+ require 'wisper'
2
+ require 'os'
3
+
1
4
  module Daigaku
2
5
  module Views
3
- require 'wisper'
4
- require 'os'
5
-
6
6
  class TaskView
7
7
  include Views
8
8
  include Wisper::Publisher
9
9
 
10
10
  TOP_BAR_TEXT = [
11
- 'Scroll with *UP KEY* and *DOWN KEY*',
12
- 'Open solution file with *o*',
13
- 'Verify solution with *v*',
14
- 'Clear validation with *c*',
15
- 'Exit with *ESC*'
16
- ].join(' | ')
11
+ 'Scroll with * 🠕 * and * 🠗 *',
12
+ 'Open solution file with *o*',
13
+ 'Verify solution with *v*',
14
+ 'Clear validation with *c*',
15
+ 'Exit with *Esc*'
16
+ ].join(' | ').freeze
17
17
 
18
18
  def initialize
19
19
  @lines = []
20
- @top = nil
20
+ @top = nil
21
21
  end
22
22
 
23
23
  def enter(course, chapter, unit)
24
- @course = course
24
+ @course = course
25
25
  @chapter = chapter
26
- @unit = unit
26
+ @unit = unit
27
27
 
28
28
  @test_result_lines = nil
29
- @lines = @unit.task.markdown.lines
30
- @top_bar_height = 4
31
- @head_height = 2
29
+ @lines = @unit.task.markdown.lines
30
+ @top_bar_height = 4
31
+ @head_height = 2
32
32
 
33
33
  initialize_window(@lines.count + @top_bar_height + @head_height)
34
34
  end
35
35
 
36
36
  private
37
37
 
38
- def set_head(window)
38
+ def print_head(window)
39
39
  window.setpos(0, 1)
40
40
  window.clear_line
41
41
 
@@ -55,7 +55,7 @@ module Daigaku
55
55
  def draw(window)
56
56
  @top = 0
57
57
  window.attrset(A_NORMAL)
58
- set_head(window)
58
+ print_head(window)
59
59
 
60
60
  @lines.each_with_index do |line, index|
61
61
  window.setpos(index + 2, 1)
@@ -67,24 +67,23 @@ module Daigaku
67
67
  end
68
68
 
69
69
  def scroll_up(window)
70
- if @top > 0
71
- window.scrl(-1)
72
- set_head(window)
70
+ return unless @top > 0
73
71
 
74
- @top -= 1
75
- line = @lines[@top]
72
+ window.scrl(-1)
73
+ print_head(window)
76
74
 
77
- if line
78
- window.setpos(2, 1)
79
- print_line(window, line, @top)
80
- end
81
- end
75
+ @top -= 1
76
+ line = @lines[@top]
77
+
78
+ return unless line
79
+ window.setpos(2, 1)
80
+ print_line(window, line, @top)
82
81
  end
83
82
 
84
83
  def scroll_down(window)
85
84
  if @top + Curses.lines <= @lines.count + @top_bar_height + @head_height
86
85
  window.scrl(1)
87
- set_head(window)
86
+ print_head(window)
88
87
 
89
88
  @top += 1
90
89
  line = @lines[@top + window.maxy - 1]
@@ -125,47 +124,47 @@ module Daigaku
125
124
  scrollable = true
126
125
 
127
126
  case char
128
- when 'v' # verify
129
- print_test_results(window)
130
- return
131
- when 'c' # clear
132
- reset_screen(window)
133
- return
134
- when 'o' # open solution file
135
- open_editor(@unit.solution.path)
136
- when Curses::KEY_DOWN, Curses::KEY_CTRL_N
137
- scrollable = scroll_down(window)
138
- when Curses::KEY_UP, Curses::KEY_CTRL_P
139
- scrollable = scroll_up(window)
140
- when Curses::KEY_NPAGE, ?\s # white space
141
- 0.upto(window.maxy - 2) do |n|
142
- scrolled = scroll_down(window)
143
-
144
- unless scrolled
145
- scrollable = false if n == 0
146
- break
147
- end
148
- end
149
- when Curses::KEY_PPAGE
150
- 0.upto(window.maxy - 2) do |n|
151
- scrolled = scroll_up(window)
152
-
153
- unless scrolled
154
- scrollable = false if n == 0
155
- break
156
- end
157
- end
158
- when Curses::KEY_LEFT, Curses::KEY_CTRL_T
159
- while scroll_up(window)
127
+ when 'v' # verify
128
+ print_test_results
129
+ return
130
+ when 'c' # clear
131
+ reset_screen
132
+ return
133
+ when 'o' # open solution file
134
+ open_editor(@unit.solution.path)
135
+ when Curses::KEY_DOWN, Curses::KEY_CTRL_N
136
+ scrollable = scroll_down(window)
137
+ when Curses::KEY_UP, Curses::KEY_CTRL_P
138
+ scrollable = scroll_up(window)
139
+ when Curses::KEY_NPAGE, '?\s' # white space
140
+ 0.upto(window.maxy - 2) do |n|
141
+ scrolled = scroll_down(window)
142
+
143
+ unless scrolled
144
+ scrollable = false if n == 0
145
+ break
160
146
  end
161
- when Curses::KEY_RIGHT, Curses::KEY_CTRL_B
162
- while scroll_down(window)
147
+ end
148
+ when Curses::KEY_PPAGE
149
+ 0.upto(window.maxy - 2) do |n|
150
+ scrolled = scroll_up(window)
151
+
152
+ unless scrolled
153
+ scrollable = false if n == 0
154
+ break
163
155
  end
164
- when Curses::KEY_BACKSPACE
165
- broadcast(:reenter, @course, @chapter, @unit)
166
- return
167
- when 27 # ESC
168
- exit
156
+ end
157
+ when Curses::KEY_LEFT, Curses::KEY_CTRL_T
158
+ while scroll_up(window)
159
+ end
160
+ when Curses::KEY_RIGHT, Curses::KEY_CTRL_B
161
+ while scroll_down(window)
162
+ end
163
+ when Curses::KEY_BACKSPACE
164
+ broadcast(:reenter, @course, @chapter, @unit)
165
+ return
166
+ when 27 # ESC
167
+ exit
169
168
  end
170
169
 
171
170
  Curses.beep unless scrollable
@@ -184,16 +183,16 @@ module Daigaku
184
183
  end
185
184
  end
186
185
 
187
- def print_test_results(window)
188
- result = @unit.solution.verify!
189
- @test_result_lines = result.summary_lines
186
+ def print_test_results
187
+ result = @unit.solution.verify!
188
+ @test_result_lines = test_result_lines(result)
190
189
 
191
190
  if result.passed?
192
191
  code_lines = @unit.reference_solution.code_lines
193
192
 
194
193
  unless code_lines.empty?
195
194
  code_lines.map! { |line| " #{line}" }
196
- code_lines.unshift('', "Reference code:", '')
195
+ code_lines.unshift('', 'Reference code:', '')
197
196
  end
198
197
 
199
198
  @test_result_lines += code_lines
@@ -202,21 +201,40 @@ module Daigaku
202
201
  @lines = [''] + @test_result_lines + ['', ''] + @unit.task.markdown.lines
203
202
  @examples = result.examples
204
203
 
205
- @example_heights = @examples.reduce({}) do |hash, example|
206
- start = hash.values.reduce(0) { |sum, r| sum += r.count }
204
+ @example_heights = @examples.each_with_object({}) do |example, hash|
205
+ start = hash.values.reduce(0) { |sum, results| sum + results.count }
207
206
  range = (start..(start + example.message.split("\n").count) + 2)
208
207
  hash[hash.keys.count] = range
209
- hash
210
208
  end
211
209
 
212
- height = [@lines.count + @top_bar_height + @head_height, Curses.lines].max
210
+ lines_count = @lines.count + @top_bar_height + @head_height
211
+ height = [lines_count, Curses.lines].max
212
+
213
213
  initialize_window(height)
214
214
  end
215
215
 
216
- def reset_screen(window)
216
+ def test_result_lines(result)
217
+ lines = result.summary_lines
218
+ lines += code_error_lines(@unit.solution.code) unless result.passed?
219
+ lines
220
+ end
221
+
222
+ def code_error_lines(code)
223
+ begin
224
+ eval(code)
225
+ rescue StandardError, ScriptError => error
226
+ return error.inspect.gsub(/(\A.*#<|>.*$)/, '').lines.map(&:rstrip)
227
+ end
228
+
229
+ []
230
+ end
231
+
232
+ def reset_screen
217
233
  @test_result_lines = nil
218
- @lines = @unit.task.markdown.lines
219
- height = [@lines.count + @top_bar_height + @head_height, Curses.lines].max
234
+ @lines = @unit.task.markdown.lines
235
+ lines_count = @lines.count + @top_bar_height + @head_height
236
+ height = [lines_count, Curses.lines].max
237
+
220
238
  initialize_window(height)
221
239
  end
222
240
 
@@ -228,9 +246,8 @@ module Daigaku
228
246
  end
229
247
 
230
248
  def example_index(heights, index)
231
- heights.values.index { |range| range.include?(index) }
249
+ heights.values.index { |range| range.include?(index) }.to_i
232
250
  end
233
251
  end
234
-
235
252
  end
236
253
  end