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
@@ -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