daigaku 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +7 -4
- data/CODE_OF_CONDUCT.md +77 -0
- data/README.md +11 -11
- data/bin/daigaku +6 -2
- data/daigaku.gemspec +22 -26
- data/lib/daigaku.rb +0 -1
- data/lib/daigaku/chapter.rb +3 -4
- data/lib/daigaku/coloring.rb +22 -27
- data/lib/daigaku/configuration.rb +25 -28
- data/lib/daigaku/congratulator.rb +17 -5
- data/lib/daigaku/course.rb +9 -8
- data/lib/daigaku/exceptions.rb +0 -2
- data/lib/daigaku/generator.rb +13 -15
- data/lib/daigaku/github_client.rb +5 -5
- data/lib/daigaku/loadable.rb +23 -14
- data/lib/daigaku/loading/chapters.rb +0 -2
- data/lib/daigaku/loading/courses.rb +0 -2
- data/lib/daigaku/loading/units.rb +0 -2
- data/lib/daigaku/markdown.rb +1 -0
- data/lib/daigaku/markdown/printer.rb +89 -0
- data/lib/daigaku/markdown/ruby_doc.rb +15 -15
- data/lib/daigaku/solution.rb +23 -15
- data/lib/daigaku/storeable.rb +11 -12
- data/lib/daigaku/task.rb +1 -1
- data/lib/daigaku/terminal.rb +3 -4
- data/lib/daigaku/terminal/cli.rb +8 -8
- data/lib/daigaku/terminal/courses.rb +22 -19
- data/lib/daigaku/terminal/output.rb +46 -53
- data/lib/daigaku/terminal/setup.rb +13 -18
- data/lib/daigaku/terminal/solutions.rb +27 -32
- data/lib/daigaku/terminal/welcome.rb +9 -11
- data/lib/daigaku/test.rb +7 -10
- data/lib/daigaku/test_result.rb +54 -21
- data/lib/daigaku/unit.rb +2 -4
- data/lib/daigaku/version.rb +1 -1
- data/lib/daigaku/views.rb +29 -33
- data/lib/daigaku/views/chapters_menu.rb +16 -20
- data/lib/daigaku/views/courses_menu.rb +12 -15
- data/lib/daigaku/views/main_menu.rb +23 -23
- data/lib/daigaku/views/menu.rb +14 -18
- data/lib/daigaku/views/splash.rb +11 -13
- data/lib/daigaku/views/subscriber.rb +38 -0
- data/lib/daigaku/views/task_view.rb +97 -80
- data/lib/daigaku/views/top_bar.rb +4 -10
- data/lib/daigaku/views/units_menu.rb +16 -21
- data/lib/daigaku/window.rb +12 -70
- data/spec/daigaku/chapter_spec.rb +23 -18
- data/spec/daigaku/coloring_spec.rb +0 -1
- data/spec/daigaku/configuration_spec.rb +54 -50
- data/spec/daigaku/congratulator_spec.rb +11 -8
- data/spec/daigaku/course_spec.rb +75 -52
- data/spec/daigaku/generator_spec.rb +24 -25
- data/spec/daigaku/github_client_spec.rb +17 -18
- data/spec/daigaku/loading/chapters_spec.rb +2 -3
- data/spec/daigaku/loading/courses_spec.rb +2 -3
- data/spec/daigaku/loading/units_spec.rb +4 -5
- data/spec/daigaku/markdown/ruby_doc_spec.rb +12 -6
- data/spec/daigaku/reference_solution_spec.rb +8 -10
- data/spec/daigaku/solution_spec.rb +21 -22
- data/spec/daigaku/storeable_spec.rb +12 -10
- data/spec/daigaku/task_spec.rb +3 -4
- data/spec/daigaku/terminal/cli_spec.rb +29 -21
- data/spec/daigaku/terminal/courses_spec.rb +104 -99
- data/spec/daigaku/terminal/output_spec.rb +44 -39
- data/spec/daigaku/terminal/setup_spec.rb +1 -3
- data/spec/daigaku/terminal/solutions_spec.rb +0 -2
- data/spec/daigaku/terminal/welcome_spec.rb +0 -2
- data/spec/daigaku/terminal_spec.rb +5 -7
- data/spec/daigaku/test_example_spec.rb +16 -14
- data/spec/daigaku/test_result_spec.rb +21 -25
- data/spec/daigaku/test_spec.rb +11 -12
- data/spec/daigaku/unit_spec.rb +24 -27
- data/spec/daigaku/views/chapters_menu_spec.rb +0 -1
- data/spec/daigaku/views/courses_menu_spec.rb +0 -1
- data/spec/daigaku/views/menu_spec.rb +1 -2
- data/spec/daigaku/views/task_view_spec.rb +0 -2
- data/spec/daigaku/views/units_menu_spec.rb +0 -1
- data/spec/daigaku/views_spec.rb +0 -1
- data/spec/daigaku_spec.rb +9 -12
- data/spec/path_helpers_spec.rb +11 -12
- data/spec/resource_helpers_spec.rb +11 -12
- data/spec/spec_helper.rb +3 -4
- data/spec/support/macros/content_helpers.rb +16 -17
- data/spec/support/macros/mock_helpers.rb +6 -6
- data/spec/support/macros/path_helpers.rb +15 -15
- data/spec/support/macros/resource_helpers.rb +34 -35
- metadata +32 -44
@@ -1,13 +1,25 @@
|
|
1
1
|
module Daigaku
|
2
|
-
|
3
2
|
class Congratulator
|
4
|
-
|
5
3
|
def self.message
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
data/lib/daigaku/course.rb
CHANGED
@@ -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
|
10
|
-
@title
|
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.
|
18
|
+
chapters.any?(&:started?)
|
20
19
|
end
|
21
20
|
|
22
21
|
def mastered?
|
23
|
-
chapters.
|
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
|
67
|
+
rescue StandardError => e
|
70
68
|
puts e
|
71
69
|
old_dir = "#{course_dir}_old/"
|
72
|
-
|
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)
|
data/lib/daigaku/exceptions.rb
CHANGED
@@ -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
|
data/lib/daigaku/generator.rb
CHANGED
@@ -1,21 +1,21 @@
|
|
1
|
-
|
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,
|
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
|
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
|
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
|
29
|
-
base_dir
|
30
|
-
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.
|
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.
|
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
|
-
|
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
|
25
|
-
stored_time
|
26
|
-
current_time =
|
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
|
data/lib/daigaku/loadable.rb
CHANGED
@@ -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
|
-
|
8
|
-
dirs = Dir.entries(path).select do |entry|
|
9
|
-
!entry.match(/\./)
|
10
|
-
end
|
4
|
+
return [] unless Dir.exist?(path)
|
11
5
|
|
12
|
-
|
13
|
-
|
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
|
data/lib/daigaku/markdown.rb
CHANGED
@@ -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
|
-
|
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?(.*)\)
|
12
|
-
STDLIB_REGEX = /\(ruby-doc stdlib:\s?(.*)\)
|
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
|
-
|
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
|
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
|
58
|
-
method
|
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
|
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
|
90
|
-
method_name = CGI.escape(method.strip).
|
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
|
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
|