kielce 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 38341d1480aea6176a4d3d289f543a82c6846464268057cce43704b0c04fe811
4
+ data.tar.gz: 711eca88ec683a63c017c326ff9169e5c14f8456cc91a499cac68ce77071b79c
5
+ SHA512:
6
+ metadata.gz: 9359354d455c3e81e46d57f5a59da179137d60c016544cf57887677f67f5da578ccccf2e720a1c007703ddeaab093cd8c1403f417c947775220386722c9311ea
7
+ data.tar.gz: b62f1d98c6309e32e4b97f26337c4538b8ed6e4b750bd399fc61d925768ce0e4f1b451f3542c40d9d3c92f69afc9807e972d7a177a2611137d2b5bfdd31757d6
@@ -0,0 +1,4 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'kielce'
4
+ Kielce::run
@@ -0,0 +1,87 @@
1
+ require "optparse"
2
+
3
+ require "kielce/helpers"
4
+ require "kielce/kielce_data"
5
+ require "kielce/kielce_loader"
6
+ require "kielce/kielce"
7
+
8
+ ##############################################################################################
9
+ #
10
+ # Main (the run method)
11
+ #
12
+ # (c) 2020 Zachary Kurmas
13
+ #
14
+ ##############################################################################################
15
+
16
+ module Kielce
17
+ VERSION = "2.0.0"
18
+
19
+ def self.run
20
+
21
+ # TODO:
22
+ #idea: If 1 param: Assume input and stdout
23
+ # If 2 params and 2nd param is file, assume input and output
24
+ # If 2+ params and 2nd param is dir, then process all files. Place in output dir. remove.erb from filename.
25
+
26
+ options = {
27
+ quiet: false,
28
+ }
29
+
30
+ OptionParser.new do |opts|
31
+ opts.banner = "Usage: kielce [options]"
32
+
33
+ opts.on("-q", "--[no-]quiet", "Run quietly") do |q|
34
+ options[:quiet] = q
35
+ end
36
+ end.parse!
37
+
38
+ $stderr.puts "KielceRB (version #{VERSION})" unless options[:quiet]
39
+
40
+ if ARGV.length == 0
41
+ $stderr.puts "Usage: kielce file_to_process"
42
+ exit
43
+ end
44
+
45
+ #
46
+ # Get to work
47
+ #
48
+
49
+ file = ARGV[0]
50
+ $stderr.puts "Processing #{file}" unless options[:quiet]
51
+
52
+ # We use global variables $d and $k to make the data and +Kielce+ objects available to the
53
+ # ERB template engine. There was a bit of debate about this decision. The most popular
54
+ # alternative was to create a class named +DataContext+ with +data+ and +kielce+ accessors.
55
+ #
56
+ # Advantages to using global variables:
57
+ # (1) The resulting names were short (two characters) and eye-catching (because the first
58
+ # character is '$')
59
+ # (2) It keeps the design simpler by eliminating the need for an additional +DataContext+ class.
60
+ # (3) Users can create a custom class to use as a context without worring about conforiming to
61
+ # any specific interface
62
+ #
63
+ # Advantages to using a special +DataContext+ class:
64
+ # (1) Avoids polluting the global namespace.
65
+
66
+ begin
67
+ context = Object.new
68
+ $d = KielceLoader.load(file, context: context)
69
+ $k = Kielce.new(context)
70
+ result = $k.render(file)
71
+ puts result if $k.error_count == 0
72
+ rescue LoadingError => e
73
+ $stderr.puts e.message
74
+ exit 1
75
+ rescue IncompleteRenderError => e2
76
+ part2 = e2.source_file.nil? ? "" : "(included from #{e2.source_file})"
77
+ $stderr.puts "ERROR: Unable to read #{e2.file_name} #{part2}"
78
+ $stderr.puts "\t(#{e2.source_exception})"
79
+ exit 1
80
+ end
81
+ exit $k.error_count == 0 ? 0 : 1
82
+ end
83
+ end
84
+
85
+ # For now, we just require all plugins here. Should we ever get to the point where there
86
+ # are enough plugins to cause a performance issue, we can do something more clever.
87
+ # require_relative 'kielce_plugins/schedule'
@@ -0,0 +1,29 @@
1
+ ##############################################################################################
2
+ #
3
+ # Helpers
4
+ #
5
+ # (c) 2020 Zachary Kurmas
6
+ #
7
+ ##############################################################################################
8
+
9
+ class ::String
10
+ def link(text = nil)
11
+ $k.link(self, text)
12
+ #%Q(<a href="#{self}">#{text}</a>)
13
+ end
14
+ end
15
+
16
+ class ::Hash
17
+ # Merges two hashes recursively. Specificaly, if the value of one item in the hash is itself a hash,
18
+ # merge that nested hash.
19
+ #
20
+ # Hash#merge by default returns a new Hash containing the key/value pairs of both hashes.
21
+ # If a key appears in both "self" and "second", the value in "second" takes precedence.
22
+ # Alternately, you can specify a block to be called if a key appears in both Hashes.
23
+ # The "merger" lambda below merges two nested hashes instead of simply choosing the version in
24
+ # "second"
25
+ def deep_merge(second)
26
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
27
+ self.merge(second, &merger)
28
+ end
29
+ end
@@ -0,0 +1,113 @@
1
+ ##############################################################################################
2
+ #
3
+ # Kielce
4
+ #
5
+ # Convenience methods available within the ERB templates.
6
+ #
7
+ # (c) 2020 Zachary Kurmas
8
+ #
9
+ ##############################################################################################
10
+
11
+ require "erb"
12
+
13
+ module Kielce
14
+ class IncompleteRenderError < StandardError
15
+ attr_accessor :file_name, :source_file, :source_exception
16
+
17
+ def initialize(file_to_load, source, ex)
18
+ @file_name = file_to_load
19
+ @source_file = source
20
+ @source_exception = ex
21
+ end
22
+ end
23
+
24
+ class Kielce
25
+ attr_reader :error_count
26
+ @@render_count = 0
27
+
28
+ # Constructor
29
+ #
30
+ # @param context default context to use when calling +render+
31
+ def initialize(context)
32
+ @data_context = context
33
+ @error_count = 0
34
+ @file_stack = []
35
+ end
36
+
37
+ # Generate an +href+ tag.
38
+ #
39
+ # This could be a class method. We make it
40
+ # an instance method so it can be used using the "$k.link" syntax.
41
+ def link(url, text_param = nil, code: nil, classes: nil)
42
+ class_list = classes.nil? ? "" : " class='#{classes}'"
43
+
44
+ # Make the text the same as the URL if no text given
45
+ text = text_param.nil? ? url : text_param
46
+
47
+ # use <code> if either (1) user requests it, or (2) no text_param given and no code param given
48
+ text = "<code>#{text}</code>" if code || (text_param.nil? && code.nil?)
49
+
50
+ "<a href='#{url}'#{class_list}>#{text}</a>"
51
+ end
52
+
53
+ # Load +file+ and run ERB. The binding parameter determines
54
+ # the context the .erb code runs in. The context determines
55
+ # the variables and methods available. By default, kielce
56
+ # uses the same context for loading and rendering. However,
57
+ # users can override this behavior by providing different contexts
58
+ #
59
+ #
60
+ # @param the file
61
+ # @param b the binding that the template code runs in.
62
+ # @return a +String+ containing the rendered contents
63
+ def render(file, local_variables = {}, b = @data_context.instance_exec { binding })
64
+
65
+ local_variables.each_pair do |key, value|
66
+ b.local_variable_set(key, value)
67
+ end
68
+
69
+ # $stderr.puts "In render: #{b.inspect}"
70
+ result = "<!--- ERROR -->"
71
+
72
+ begin
73
+ content = File.read(file)
74
+ rescue Errno::ENOENT => e
75
+ # TODO Consider using e.backtrace_locations instead of @file_stack
76
+ # (Question: What exaclty do you want to see if render_relative is called
77
+ # by a method)
78
+ raise IncompleteRenderError.new(file, @file_stack.last, e)
79
+ end
80
+ @file_stack.push(file)
81
+
82
+ # The two nil parameters below are legacy settings that don't
83
+ # apply to Kielce. nil is the default value. We must specify
84
+ # nil, so we can set the fourth parameter (described below).
85
+ #
86
+ # It is possible for code inside an erb file to load and render
87
+ # another erb template. In order for such nested calls to work
88
+ # properly, each call must have a unique variable in which to
89
+ # store its output. This parameter is called "eoutvar". (If you
90
+ # don't specifiy eoutvar and make a nested call, the output
91
+ # can get messed up.)
92
+ @@render_count += 1
93
+
94
+ begin
95
+ erb = ERB.new(content, nil, nil, "render_out_#{@@render_count}")
96
+ erb.filename = file.to_s
97
+ result = erb.result(b)
98
+ rescue NoKeyError => e
99
+ line_num = e.backtrace_locations.select { |i| i.path == file }.first.lineno
100
+ $stderr.puts "Unrecognized key #{e.name} at #{file}:#{line_num}"
101
+ @error_count += 1
102
+ ensure
103
+ @file_stack.pop
104
+ end
105
+ result
106
+ end
107
+
108
+ def render_relative(file, local_variables = {}, b = @data_context.instance_exec { binding })
109
+ path = Pathname.new(File.absolute_path(@file_stack.last)).dirname
110
+ render(path.join(file), local_variables, b)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,151 @@
1
+ ##############################################################################################
2
+ #
3
+ # KielceData
4
+ #
5
+ # An object representing the data loaded from the various Kielce data files.
6
+ # The files return a Hash: a set of key-value pairs.
7
+ # +KielceData+ allows users to access the data using the more convenient method syntax (i.e.,
8
+ # $d.itemName instead of $d[:itemName] or $d['itemName])
9
+ # +KielceData+ is a child of +BasicObject+ instead of +Object+ so that there aren't conflicts
10
+ # with methods defined on +Object+ and +Kernel+ (+freeze+, +method+, +trust+, etc. )
11
+ #
12
+ # For convenience, +KielceData+ does provide a few key methods including +is_a+ and +inspect+
13
+ #
14
+ # (c) 2020 Zachary Kurmas
15
+ #
16
+ ##############################################################################################
17
+
18
+ module Kielce
19
+ class NoKeyError < ::NameError
20
+ end
21
+
22
+ class InvalidKeyError < ::NameError
23
+ end
24
+
25
+ INVALID_KEYS = [:root, :method_missing, :inspect]
26
+
27
+ # Access KielceData instance variables.
28
+ # (We don't want public accessors on the KielceData object because the name
29
+ # we choose may potentially conflict with user data.)
30
+ class KielceDataAnalyzer
31
+ class << self
32
+ def root(obj)
33
+ obj.instance_eval { @xx_kielce_root }
34
+ end
35
+
36
+ def name(obj)
37
+ obj.instance_eval { @xx_kielce_obj_name }
38
+ end
39
+
40
+ def data(obj)
41
+ obj.instance_eval { @xx_kielce_data }
42
+ end
43
+ end
44
+ end
45
+
46
+ # By extending BasicObject instead of Object, we don't have to worry
47
+ # about user data conflicting with method names in Object or Kernel
48
+ #
49
+ # @xx_kielce_data is the hash passed to the constructor.
50
+ # @xx_kielce_obj_name is the key that maps to the value.
51
+ # @xx_kielce_root refers to the root object. It is used so that lambdas have access to
52
+ # the data.
53
+ #
54
+ # (The strange "xx_kielce_" naming is to avoid conflicts with user-chosen names)
55
+ class KielceData < BasicObject
56
+ @@error_output = $stderr
57
+
58
+ def self.error_output
59
+ @@error_output
60
+ end
61
+
62
+ def self.error_output=(val)
63
+ @@error_output=val
64
+ end
65
+
66
+ def initialize(data, root = nil, obj_name = nil)
67
+
68
+ INVALID_KEYS.each do |key|
69
+ ::Kernel.send(:raise, InvalidKeyError.new("Invalid Key: #{key} may not be used as a key.", key)) if data.has_key?(key)
70
+ end
71
+
72
+ @xx_kielce_obj_name = obj_name.nil? ? "" : obj_name
73
+
74
+ @xx_kielce_data = data
75
+
76
+ # Remember, root may be a BasicObject and, therefore, not define .nil
77
+ @xx_kielce_root = (root == nil) ? self : root
78
+
79
+ @xx_kielce_data.each do |key, value|
80
+ if value.is_a?(::Hash)
81
+ @xx_kielce_data[key] = ::Kielce::KielceData.new(value, @xx_kielce_root, "#{@xx_kielce_obj_name}#{key}.")
82
+ end
83
+ end
84
+ end
85
+
86
+ def root
87
+ @xx_kielce_root
88
+ end
89
+
90
+ def inspect
91
+ "KielceData: #{@xx_kielce_obj_name} #{@xx_kielce_data.inspect}"
92
+ end
93
+
94
+ def is_a?(klass)
95
+ klass == ::Kielce::KielceData
96
+ end
97
+
98
+ # Provides the "magic" that allows users to access data using method syntax.
99
+ # This method is called whenever a method that doesn't exist is invoked. It
100
+ # then looks through the hash for a key with the same name as the method.
101
+ def method_missing(name, *args, **keyword_args, &block)
102
+
103
+ # $stderr.puts "Processing #{name} in #{@xx_kielce_obj_name}"
104
+
105
+ # Convert the name to a symbol. (It is probably already a symbol, but the extra .to_sym won't hurt)
106
+ # Then complian if there isn't a data object by that name.
107
+ name_sym = name.to_sym
108
+ full_name = "#{@xx_kielce_obj_name}#{name}"
109
+ unless @xx_kielce_data.has_key?(name_sym)
110
+ # The first ("message") parameter is currently unused. The message can be changed, if desired.
111
+ ::Kernel.send(:raise, NoKeyError.new("Unrecognized Key: #{full_name}", full_name))
112
+ end
113
+
114
+ # Get the requested data object.
115
+ # If the object is a lambda, execute the lambda.
116
+ # Otherwise, just return it.
117
+ item = @xx_kielce_data[name_sym]
118
+ if item.is_a?(::Proc)
119
+ if item.parameters.any? { |i| i.last == :root }
120
+ @@error_output.puts 'WARNING! Lambda parameter named root shadows instance method root.'
121
+ end
122
+
123
+ #$stderr.puts item.parameters.inspect
124
+ keyword_params = item.parameters.select { |i| i.first == :keyreq || i.first == :key }
125
+ num_keyword = keyword_params.size
126
+ num_args = item.parameters.size - num_keyword
127
+
128
+ #$stderr.puts "-----------"
129
+ #$stderr.puts args.inspect
130
+ #$stderr.puts keyword_args.inspect
131
+ #$stderr.puts "Num each: #{num_args} #{num_keyword}"
132
+
133
+ if num_args == 0 && num_keyword == 0
134
+ return instance_exec(&item)
135
+ elsif num_args > 0 && num_keyword == 0
136
+ return instance_exec(*args, &item)
137
+ elsif num_keyword > 0
138
+ return instance_exec(*args, **keyword_args, &item)
139
+ else
140
+ $stderr.puts "FAIL. Shouldn't get here!"
141
+ end
142
+ end
143
+
144
+ if args.length != 0 || keyword_args.size != 0
145
+ @@error_output.puts "WARNING! #{full_name} is not a function and doesn't expect parameters."
146
+ end
147
+
148
+ @xx_kielce_data[name_sym]
149
+ end
150
+ end # class
151
+ end # module
@@ -0,0 +1,144 @@
1
+ ##############################################################################################
2
+ #
3
+ # KielceLoader
4
+ #
5
+ # Methods to find and load Kielce data files.
6
+ #
7
+ # (c) 2020 Zachary Kurmas
8
+ #
9
+ ##############################################################################################
10
+
11
+ require 'pathname'
12
+
13
+ module Kielce
14
+
15
+ class LoadingError < StandardError
16
+ end
17
+
18
+ class KielceLoader
19
+
20
+ # Load the kielce_data_*.rb files beginning from the directory that contains +start+.
21
+ #
22
+ # @param start the name of the file in the directory where the search for data files should begin
23
+ # @param current the current data object (which may contain data loaded from other files)
24
+ # @param context the context in which the data files will be executed.
25
+ # @return a +KielceData+ object containing the collective merged data from all kielce data files.
26
+ #
27
+ # Notice that +start+ need not be either a directory or a data file. Instead we use
28
+ # +start+'s full pathname to identify the directory where we will begin our search.
29
+ # We allow +start+ to be any file because, in most cases, it is simply the .erb file
30
+ # being processed -- thus, it's easiest/cleaner for the user to pass this filename
31
+ # as a parameter rather than convert it to a starting directory first.
32
+ #
33
+ def self.load(start, current: {}, context: Object.new, stop_dir: nil)
34
+ # File.absolute_path returns a String. We need a full Pathname object.
35
+ stop_path = nil
36
+ stop_path = Pathname.new(File.absolute_path(stop_dir)) unless stop_dir.nil?
37
+ start_path = Pathname.new(File.absolute_path(start))
38
+ start_path = start_path.parent unless start_path.directory?
39
+ KielceData.new(load_directory_raw(start_path, current: current, context: context, stop_dir: stop_path))
40
+ end
41
+
42
+ # loads a kielce_data_*.rb file and returns the file's raw data (the plain Ruby Hash)
43
+ #
44
+ # @param file the file to load
45
+ # @param current the current data object (which may contain data loaded from other files)
46
+ # @param context the context in which to execute the data file's code
47
+ # @return the updated data object.
48
+ #
49
+ def self.load_file_raw(file, current: {}, context: Object.new)
50
+ #$stderr.puts "Processing #{file}"
51
+
52
+ # (1) Kielce data files are assumed to return a single Hash.
53
+ # (2) The second parameter to eval, the "binding", is an object
54
+ # describing the context the code being evaluated will run in.
55
+ # Among other things, this context determines the local variables
56
+ # and methods avaiable to the code being evaluated. In order to
57
+ # prevent data files from manipuating this KielceLoader object this
58
+ # method's local variables, we create an empty object and use its
59
+ # binding. Note, however, that the code can still read and set global
60
+ # variables. Users can also provide a different, custom, context object.
61
+ # (3) To get the correct binding, we need to run the call to +binding+
62
+ # in the context of an instance method on the context object.
63
+ # The "obvious" way to do this is to add a method to the object
64
+ # that calls and returns binding. We decided not to do this for
65
+ # two reasons: (a) We didn't want to add any methods to the context object
66
+ # that may possibly conflict with the names chosen by users in the data
67
+ # files, and (b) Object is the default object used for context; but
68
+ # in theory, users of the library could chose a different object. We didn't want
69
+ # our chosen name to conflict with a method that may already exist on the user's
70
+ # chose context object.
71
+ # (4) The third parameter, +file+, allows Ruby to identify the source of any
72
+ # errors encountered during the evaluation process.
73
+ b = context.instance_eval { binding }
74
+ data = eval File.read(file), b, file
75
+ #$stderr.puts "Current is #{current}"
76
+ #$stderr.puts "Data is #{data}"
77
+
78
+ # If the file is empty, or has a nil return, just use an empty hash
79
+ data = {} if data.nil?
80
+
81
+ unless data.is_a?(Hash)
82
+ raise LoadingError, "ERROR: Data file #{file} did not return a Hash. It returned #{data.inspect}."
83
+ # $stderr.puts "ERROR: Datafile #{file} did not return a Hash. It returned \"#{data.inspect}\"."
84
+ exit 0
85
+ end
86
+
87
+ # These INVALID_KEYS correspond to methods on the KielceData object.
88
+ # These keys would be shadowed by these methods and produce strange errors.
89
+ # From a design perspective, I would prefer to do this check inside the KielceData class; but,
90
+ # at that point it's no longer possible to determine which file contained the invalid key.
91
+ # Thus, but repeating the check here, we can provide a better error message to the user.
92
+ INVALID_KEYS.each do |key|
93
+ if data.has_key?(key)
94
+ raise LoadingError, "ERROR: Data file #{file} uses the key \"#{key}\", which is not allowed."
95
+ exit 0
96
+ end
97
+ end
98
+
99
+
100
+ # The deep_merge method will use the data from +data+ in the event of duplicate keys.
101
+ # helpers.rb opens the Hash class and adds this method.
102
+ x = current.deep_merge(data)
103
+ #$stderr.puts "Post-merge is #{x}"
104
+ x
105
+ end
106
+
107
+ #
108
+ # Search +dir+ and all parent directories for kielce data files, load them, and
109
+ # return the raw Hash containing the collective datqa.
110
+ #
111
+ # @param dir the directory to search (as a +Pathname+ object)
112
+ # @param current the current data object
113
+ # @param context the context in which to execute the data files' code
114
+ # @pram stop_dir the last (highest-level) directory to examine (as a +Pathname+ object)
115
+ # @return the updated "raw" data object (i.e., the raw Hash)
116
+ def self.load_directory_raw(dir, current: {}, context: Object.new, stop_dir: nil)
117
+
118
+ # $stderr.puts "Done yet? #{File.absolute_path(dir)} #{File.absolute_path(stop_dir)}"
119
+
120
+ # recurse until you get to either the "stop directory" or the root of the filesystem.
121
+ # beginning from the root of the file system means that entries "closer" to the
122
+ # file being processed replace entries higher up in the file system.
123
+ # unless File.absolute_path(dir) == File.absolute_path(stop_dir) || dir.root?
124
+ unless (!stop_dir.nil? && (dir.realpath == stop_dir.realpath)) || dir.realpath.root?
125
+ current = load_directory_raw(dir.parent, current: current, context: context, stop_dir: stop_dir)
126
+ end
127
+
128
+ # By default, process all files fitting the pattern kielce_data*.rb
129
+ # In addition, look in directories named KielceData for files matching the pattern.
130
+ # (The use of KielceData directories allows users to gather multiple data files up and
131
+ # place them "out of sight")
132
+ # +dir+ is a +pathname+ object. By placing it in a string interpolation, it is coverted to
133
+ # a +String+ using +Pathname#to_s+.
134
+ # On occasion, +dir+ may actually be regular file instead of a directory. In such cases, the
135
+ # glob below will simply fail to match anything.
136
+ # QZ001
137
+ data = Dir.glob("#{dir}/KielceData/kielce_data*.rb") + Dir.glob("#{dir}/kielce_data*.rb")
138
+ data.each do |file|
139
+ current = load_file_raw(file, current: current, context: context)
140
+ end
141
+ current
142
+ end
143
+ end # class
144
+ end # module
@@ -0,0 +1,9 @@
1
+
2
+ require_relative './schedule/schedule'
3
+ require_relative './schedule/assignment'
4
+
5
+ module KielcePlugins
6
+ module Schedule
7
+ VERSION = '0.1'
8
+ end
9
+ end
@@ -0,0 +1,320 @@
1
+ require 'rubyXL'
2
+ require_relative 'assignment'
3
+
4
+ # https://www.ablebits.com/office-addins-blog/2015/03/11/change-date-format-excel/
5
+
6
+ module KielcePlugins
7
+ module Schedule
8
+
9
+ class Schedule
10
+
11
+ #SCHEDULE_KEYS = [:week, :date, :topics, :notes, :reading, :milestones, :comments]
12
+ SCHEDULE_KEYS = [:week, :date, :topics, :reading, :milestones, :comments]
13
+
14
+ attr_accessor :assignments, :schedule_days
15
+
16
+ def initialize(filename)
17
+ workbook = RubyXL::Parser.parse(filename)
18
+ @assignments = build_assignments(build_rows(workbook['Assignments'], Assignment::ASSIGNMENT_KEYS))
19
+ @schedule_days = build_schedule_days(build_rows(workbook['Schedule'], SCHEDULE_KEYS))
20
+ end
21
+
22
+ def transform(value)
23
+ if value.is_a? String
24
+ # Replace link markup
25
+ value.gsub!(/\[\[([^\s]+)(\s+(\S.*)|\s*)\]\]/) do
26
+ text = $3.nil? ? "<code>#{$1}</code>" : $3
27
+ "<a href='#{$1}'>#{text}</a>"
28
+ end
29
+
30
+ value.gsub!(/<<(assign|due)\s+(\S.+)>>/) do
31
+ #$stderr.puts "Found assignment ref #{$1} --- #{$2} -- #{@assignments[$2].inspect}"
32
+ $stderr.puts "Assignment #{$2} not found" unless @assignments.has_key?($2)
33
+
34
+ text = @assignments[$2].title(:full, true)
35
+ if $1 == 'assign'
36
+ "Assign #{text}"
37
+ elsif $1 == 'due'
38
+ "<b>Due</b> #{text}"
39
+ else
40
+ $stderr.puts "Unexpected match #{$1}"
41
+ end
42
+ end # end gsub!
43
+
44
+ value.gsub!(/{{([^{}:]+):\s*([^{}]+)}}/) do
45
+ text = $1
46
+ link_rule = $2
47
+
48
+ if (link_rule =~ /(.*)\!(.+)/)
49
+ method = $1
50
+ param = $2
51
+
52
+ method = 'default' if method.empty?
53
+ #$stderr.puts "Found link rule =>#{method}<= #{method.empty?} =>#{param}<="
54
+
55
+ link = $d.course.notesTemplates.method_missing(method, param)
56
+ else
57
+ link = link_rule
58
+ end
59
+ "(<a href='#{link}'>#{text}</a>)"
60
+ end # end gsub
61
+
62
+ end # end if value is string
63
+ value
64
+ end
65
+
66
+ def build_rows(worksheet, keys)
67
+ # Remove the first (header) row, and any empty rows.
68
+ # Also, remove any rows after "END"
69
+ first = true
70
+ done = false
71
+ rows = []
72
+ worksheet.each do |row|
73
+ done = true if !row.nil? && !row[0].nil? && row[0].value == "END"
74
+
75
+ unless first || row.nil? || done
76
+ rows << row
77
+ end
78
+ first = false
79
+ end
80
+
81
+ # For each row, build a Hash describing the row.
82
+ rows.map do |row|
83
+ row_hash = { original: {} }
84
+ keys.each_with_index do |item, index|
85
+ row_hash[:original][item] = row[index].nil? ? nil : row[index].value
86
+ row_hash[item] = row[index].nil? ? nil : transform(row[index].value)
87
+ end
88
+ row_hash
89
+ end # end map
90
+ end
91
+
92
+ def build_assignments(rows)
93
+ assignment_hash = {}
94
+ rows.each { |row| assignment_hash[row[:id]] = Assignment.new(row) }
95
+ assignment_hash
96
+ end
97
+
98
+ def build_schedule_days(schedule_rows)
99
+ array_keys = SCHEDULE_KEYS.slice(2, SCHEDULE_KEYS.length - 2)
100
+ current_week = nil
101
+ schedule_days = []
102
+ schedule_day = nil
103
+
104
+ schedule_rows.each do |row|
105
+
106
+ # if there is a date, start a new day
107
+ unless row[:date].nil?
108
+
109
+ # push day in progress into array
110
+ schedule_days << schedule_day unless schedule_day.nil?
111
+
112
+ # create a new schedule_day Hash
113
+ schedule_day = {
114
+ begin_week: false
115
+ }
116
+
117
+ unless row[:week].nil?
118
+ current_week = row[:week]
119
+ schedule_day[:begin_week] = true
120
+ end
121
+
122
+ schedule_day[:week] = current_week
123
+ schedule_day[:date] = row[:date]
124
+ array_keys.each { |key| schedule_day[key] = [] }
125
+ end
126
+
127
+ # push non-nil values onto the corresponding array
128
+ # array_keys.each { |key| schedule_day[key] << row[key] unless row[key].nil? }
129
+ array_keys.each do |key|
130
+ val = row[key]
131
+
132
+ # skip any completely empty cells (they produce a value of nil)
133
+ # Replace a single period with a whitespace. (Thus producing an empty cell in the table)
134
+ # Similarly, treat cells beginnign with // as a comment and produce an empty cell in the table)
135
+ unless row[key].nil?
136
+ val = "&nbsp;" if val == '.' || val =~ /^\s*\/\//
137
+ schedule_day[key] << val
138
+ end
139
+ end
140
+
141
+ # Look for assignments / due dates and add information to @assignment objects
142
+ original_milestones = row[:original][:milestones]
143
+ if !original_milestones.nil? && original_milestones =~ /<<due\s+(.*)>>/
144
+ #$stderr.puts "Assignment #{$1} has due date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
145
+ @assignments[$1].due = schedule_day[:date]
146
+ end
147
+ if !original_milestones.nil? && original_milestones =~ /<<assign\s+(.*)>>/
148
+ #$stderr.puts "Assignment #{$1} has assignment date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
149
+ unless @assignments.has_key?($1)
150
+ $stderr.puts "Key #{$1} not found in #{@assignments.keys.inspect}"
151
+ end
152
+ @assignments[$1].assigned = schedule_day[:date]
153
+ end
154
+ end # end each row
155
+
156
+ schedule_days << schedule_day unless schedule_day.nil?
157
+
158
+ schedule_days
159
+ end
160
+
161
+ def timeline_table
162
+ table = []
163
+ table << <<TABLE
164
+ <table class='kielceSchedule'>
165
+ <tr>
166
+ <th>Week</th>
167
+ <th>Date</th>
168
+ <th>Topics</th>
169
+ <!-- <th>Notes</th> -->
170
+ <th>Reading</th>
171
+ <th>Milestones</th>
172
+ </tr>
173
+ TABLE
174
+
175
+ first = true
176
+ @schedule_days.each do |schedule_day|
177
+ table << '<tr>'
178
+
179
+ if schedule_day[:begin_week]
180
+ unless first
181
+ # Add a blank row of horizontal lines
182
+ table << '<td></td><td></td><td></td><td></td><td></td><td></td></tr>'
183
+ table << "<tr class='week_end'><td></td><td></td><td></td><td></td><td></td><td></td></tr>"
184
+ table << '<tr>'
185
+ end
186
+ first = false
187
+ week_value = schedule_day[:week]
188
+ else
189
+ week_value = ''
190
+ end
191
+
192
+ table << " <td class='week_column'>#{week_value}</td>"
193
+ formatted_date = schedule_day[:date].strftime("%a. %-d %b.")
194
+ table << " <td class='date_column'>#{formatted_date}</td>"
195
+ table << " <td class='topics_column'>#{schedule_day[:topics].join("<br>")}</td>"
196
+ # table << " <td class='topics_column'>#{schedule_day[:notes].join("<br>")}</td>"
197
+ table << " <td class='reading_column'>#{schedule_day[:reading].join("<br>")}</td>"
198
+ table << " <td class='milestones_column'>#{schedule_day[:milestones].join("<br>")}</td>"
199
+ table << "</tr>"
200
+ end
201
+ table << "</table>"
202
+ table.join("\n")
203
+ end
204
+
205
+ def timeline_style
206
+ <<STYLE
207
+ table.kielceSchedule {
208
+ border-collapse: separate;
209
+ border-spacing: 2px;
210
+ }
211
+
212
+ .kielceSchedule tr th {
213
+ text-align: left;
214
+ }
215
+
216
+ .kielceSchedule tr td {
217
+ vertical-align: top;
218
+ padding-right: 10px;
219
+ }
220
+
221
+ .week_column, .date_column {
222
+ white-space: nowrap;
223
+ }
224
+
225
+ .kielceSchedule tr th, .date_column, .topics_column, .notes_column, .reading_column, .milestones_column, .week_end td {
226
+ border-bottom: 1px solid;
227
+ }
228
+ STYLE
229
+ end
230
+
231
+ def timeline_page
232
+ <<PAGE
233
+ <html>
234
+ <head>
235
+ <style>
236
+ #{timeline_style}
237
+ </style>
238
+ </head>
239
+ <body>
240
+ #{timeline_table}
241
+ </body>
242
+ </html>
243
+ PAGE
244
+ end
245
+
246
+ def assignment_style
247
+ <<STYLE
248
+ .kielceAssignmentTable {
249
+ border-spacing: 35px 0;
250
+ }
251
+
252
+ .kielceAssignmentTable tr th {
253
+ text-align: left;
254
+ }
255
+
256
+ .kielceAssignmentTable tr td {
257
+ vertical-align: top;
258
+ }
259
+
260
+ .kielceAssignmentTable_due {
261
+ white-space: nowrap;
262
+ }
263
+
264
+ .kielceAssignmentTable_title {
265
+ white-space: nowrap;
266
+ }
267
+
268
+ .exam {
269
+ background-color: lightgreen;
270
+ }
271
+ STYLE
272
+ end
273
+
274
+
275
+ def assignment_list
276
+ list = <<TABLE
277
+ <table class='kielceAssignmentTable'>
278
+ <tr>
279
+ <th>Due</th>
280
+ <th>Name</th>
281
+ <th>Details</th>
282
+ </tr>
283
+ TABLE
284
+
285
+ by_date = @assignments.values.reject { |item| item.due.nil? || item.type == 'Lab' }.sort_by { |a| a.due }
286
+ by_date.each do |assignment|
287
+ list += ' <tr>'
288
+ list += " <td class='kielceAssignmentTable_due'>#{assignment.due.strftime("%a. %-d %b.")}</td>\n"
289
+ list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
290
+ list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
291
+ list += " </tr>\n\n"
292
+ end
293
+ list += "</table>\n"
294
+ end
295
+
296
+ def lab_list
297
+ list = <<TABLE
298
+ <table class='kielceAssignmentTable'>
299
+ <tr>
300
+ <th>Date</th>
301
+ <th>Name</th>
302
+ <th>Details</th>
303
+ </tr>
304
+ TABLE
305
+ assigned_labs = @assignments.values.select { |item| item.type == 'Lab' && !item.assigned.nil? }
306
+ by_date = assigned_labs.sort { |a, b| a.assigned <=> b.assigned }
307
+ by_date.each do |assignment|
308
+ list += ' <tr>'
309
+ list += " <td class='kielceAssignmentTable_due'>#{assignment.assigned.strftime("%a. %-d %b.")}</td>\n"
310
+ list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
311
+ list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
312
+ list += " </tr>\n\n"
313
+ end
314
+ list += "</table>\n"
315
+ list
316
+ end
317
+ end # end Schedule
318
+ end # module
319
+ end # end KielcePlugins
320
+ #puts Schedule.new(ARGV[0]).timeline_page
@@ -0,0 +1,58 @@
1
+ module KielcePlugins
2
+ module Schedule
3
+
4
+ class Assignment
5
+
6
+ ASSIGNMENT_KEYS = [:id, :title, :type, :number, :link, :details]
7
+
8
+ SHORT_TYPE = {
9
+ homework: 'HW',
10
+ project: 'P',
11
+ lab: 'L'
12
+ }
13
+
14
+ # create a getter for each key (some getters overridden below)
15
+ ASSIGNMENT_KEYS.each { |key| attr_reader key }
16
+
17
+ attr_accessor :due, :assigned
18
+
19
+ def initialize(row)
20
+ ASSIGNMENT_KEYS.each { |key| instance_variable_set "@#{key.to_s}".to_sym, row[key] }
21
+ end
22
+
23
+ def has_type?
24
+ !@type.nil?
25
+ end
26
+
27
+ def type
28
+ @type.to_s
29
+ end
30
+
31
+ def short_type
32
+ return '' unless has_type?
33
+ key = @type.downcase.to_sym
34
+ $stderr.puts "Unknown type #{@type}" unless SHORT_TYPE.has_key?(key)
35
+ SHORT_TYPE[key]
36
+ end
37
+
38
+ def title(style = :original, linked=false)
39
+ case style
40
+ when :original
41
+ text = @title
42
+ when :short_type
43
+ type_num = has_type? ? "#{short_type}#{@number}: " : ''
44
+ text = "#{type_num}#{@title}"
45
+ when :full
46
+ type_string = has_type? ? "#{@type} #{@number}: " : ''
47
+ text = "#{type_string}#{@title}"
48
+ else
49
+ $stderr.puts "Unknown style #{sytle}"
50
+ text = nil
51
+ end
52
+
53
+ # build a link, if a link provided
54
+ (linked && !@link.nil?) ? "<a href='#{@link}'>#{text}</a>" : text
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,320 @@
1
+ require 'rubyXL'
2
+ require_relative 'assignment'
3
+
4
+ # https://www.ablebits.com/office-addins-blog/2015/03/11/change-date-format-excel/
5
+
6
+ module KielcePlugins
7
+ module Schedule
8
+
9
+ class Schedule
10
+
11
+ #SCHEDULE_KEYS = [:week, :date, :topics, :notes, :reading, :milestones, :comments]
12
+ SCHEDULE_KEYS = [:week, :date, :topics, :reading, :milestones, :comments]
13
+
14
+ attr_accessor :assignments, :schedule_days
15
+
16
+ def initialize(filename)
17
+ workbook = RubyXL::Parser.parse(filename)
18
+ @assignments = build_assignments(build_rows(workbook['Assignments'], Assignment::ASSIGNMENT_KEYS))
19
+ @schedule_days = build_schedule_days(build_rows(workbook['Schedule'], SCHEDULE_KEYS))
20
+ end
21
+
22
+ def transform(value)
23
+ if value.is_a? String
24
+ # Replace link markup
25
+ value.gsub!(/\[\[([^\s]+)(\s+(\S.*)|\s*)\]\]/) do
26
+ text = $3.nil? ? "<code>#{$1}</code>" : $3
27
+ "<a href='#{$1}'>#{text}</a>"
28
+ end
29
+
30
+ value.gsub!(/<<(assign|due)\s+(\S.+)>>/) do
31
+ #$stderr.puts "Found assignment ref #{$1} --- #{$2} -- #{@assignments[$2].inspect}"
32
+ $stderr.puts "Assignment #{$2} not found" unless @assignments.has_key?($2)
33
+
34
+ text = @assignments[$2].title(:full, true)
35
+ if $1 == 'assign'
36
+ "Assign #{text}"
37
+ elsif $1 == 'due'
38
+ "<b>Due</b> #{text}"
39
+ else
40
+ $stderr.puts "Unexpected match #{$1}"
41
+ end
42
+ end # end gsub!
43
+
44
+ value.gsub!(/{{([^{}:]+):\s*([^{}]+)}}/) do
45
+ text = $1
46
+ link_rule = $2
47
+
48
+ if (link_rule =~ /(.*)\!(.+)/)
49
+ method = $1
50
+ param = $2
51
+
52
+ method = 'default' if method.empty?
53
+ #$stderr.puts "Found link rule =>#{method}<= #{method.empty?} =>#{param}<="
54
+
55
+ link = $d.course.notesTemplates.method_missing(method, param)
56
+ else
57
+ link = link_rule
58
+ end
59
+ "(<a href='#{link}'>#{text}</a>)"
60
+ end # end gsub
61
+
62
+ end # end if value is string
63
+ value
64
+ end
65
+
66
+ def build_rows(worksheet, keys)
67
+ # Remove the first (header) row, and any empty rows.
68
+ # Also, remove any rows after "END"
69
+ first = true
70
+ done = false
71
+ rows = []
72
+ worksheet.each do |row|
73
+ done = true if !row.nil? && !row[0].nil? && row[0].value == "END"
74
+
75
+ unless first || row.nil? || done
76
+ rows << row
77
+ end
78
+ first = false
79
+ end
80
+
81
+ # For each row, build a Hash describing the row.
82
+ rows.map do |row|
83
+ row_hash = { original: {} }
84
+ keys.each_with_index do |item, index|
85
+ row_hash[:original][item] = row[index].nil? ? nil : row[index].value
86
+ row_hash[item] = row[index].nil? ? nil : transform(row[index].value)
87
+ end
88
+ row_hash
89
+ end # end map
90
+ end
91
+
92
+ def build_assignments(rows)
93
+ assignment_hash = {}
94
+ rows.each { |row| assignment_hash[row[:id]] = Assignment.new(row) }
95
+ assignment_hash
96
+ end
97
+
98
+ def build_schedule_days(schedule_rows)
99
+ array_keys = SCHEDULE_KEYS.slice(2, SCHEDULE_KEYS.length - 2)
100
+ current_week = nil
101
+ schedule_days = []
102
+ schedule_day = nil
103
+
104
+ schedule_rows.each do |row|
105
+
106
+ # if there is a date, start a new day
107
+ unless row[:date].nil?
108
+
109
+ # push day in progress into array
110
+ schedule_days << schedule_day unless schedule_day.nil?
111
+
112
+ # create a new schedule_day Hash
113
+ schedule_day = {
114
+ begin_week: false
115
+ }
116
+
117
+ unless row[:week].nil?
118
+ current_week = row[:week]
119
+ schedule_day[:begin_week] = true
120
+ end
121
+
122
+ schedule_day[:week] = current_week
123
+ schedule_day[:date] = row[:date]
124
+ array_keys.each { |key| schedule_day[key] = [] }
125
+ end
126
+
127
+ # push non-nil values onto the corresponding array
128
+ # array_keys.each { |key| schedule_day[key] << row[key] unless row[key].nil? }
129
+ array_keys.each do |key|
130
+ val = row[key]
131
+
132
+ # skip any completely empty cells (they produce a value of nil)
133
+ # Replace a single period with a whitespace. (Thus producing an empty cell in the table)
134
+ # Similarly, treat cells beginnign with // as a comment and produce an empty cell in the table)
135
+ unless row[key].nil?
136
+ val = "&nbsp;" if val == '.' || val =~ /^\s*\/\//
137
+ schedule_day[key] << val
138
+ end
139
+ end
140
+
141
+ # Look for assignments / due dates and add information to @assignment objects
142
+ original_milestones = row[:original][:milestones]
143
+ if !original_milestones.nil? && original_milestones =~ /<<due\s+(.*)>>/
144
+ #$stderr.puts "Assignment #{$1} has due date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
145
+ @assignments[$1].due = schedule_day[:date]
146
+ end
147
+ if !original_milestones.nil? && original_milestones =~ /<<assign\s+(.*)>>/
148
+ #$stderr.puts "Assignment #{$1} has assignment date of #{schedule_day[:date].strftime("%a. %-d %b.")}"
149
+ unless @assignments.has_key?($1)
150
+ $stderr.puts "Key #{$1} not found in #{@assignments.keys.inspect}"
151
+ end
152
+ @assignments[$1].assigned = schedule_day[:date]
153
+ end
154
+ end # end each row
155
+
156
+ schedule_days << schedule_day unless schedule_day.nil?
157
+
158
+ schedule_days
159
+ end
160
+
161
+ def timeline_table
162
+ table = []
163
+ table << <<TABLE
164
+ <table class='kielceSchedule'>
165
+ <tr>
166
+ <th>Week</th>
167
+ <th>Date</th>
168
+ <th>Topics</th>
169
+ <!-- <th>Notes</th> -->
170
+ <th>Reading</th>
171
+ <th>Milestones</th>
172
+ </tr>
173
+ TABLE
174
+
175
+ first = true
176
+ @schedule_days.each do |schedule_day|
177
+ table << '<tr>'
178
+
179
+ if schedule_day[:begin_week]
180
+ unless first
181
+ # Add a blank row of horizontal lines
182
+ table << '<td></td><td></td><td></td><td></td><td></td><td></td></tr>'
183
+ table << "<tr class='week_end'><td></td><td></td><td></td><td></td><td></td><td></td></tr>"
184
+ table << '<tr>'
185
+ end
186
+ first = false
187
+ week_value = schedule_day[:week]
188
+ else
189
+ week_value = ''
190
+ end
191
+
192
+ table << " <td class='week_column'>#{week_value}</td>"
193
+ formatted_date = schedule_day[:date].strftime("%a. %-d %b.")
194
+ table << " <td class='date_column'>#{formatted_date}</td>"
195
+ table << " <td class='topics_column'>#{schedule_day[:topics].join("<br>")}</td>"
196
+ # table << " <td class='topics_column'>#{schedule_day[:notes].join("<br>")}</td>"
197
+ table << " <td class='reading_column'>#{schedule_day[:reading].join("<br>")}</td>"
198
+ table << " <td class='milestones_column'>#{schedule_day[:milestones].join("<br>")}</td>"
199
+ table << "</tr>"
200
+ end
201
+ table << "</table>"
202
+ table.join("\n")
203
+ end
204
+
205
+ def timeline_style
206
+ <<STYLE
207
+ table.kielceSchedule {
208
+ border-collapse: separate;
209
+ border-spacing: 2px;
210
+ }
211
+
212
+ .kielceSchedule tr th {
213
+ text-align: left;
214
+ }
215
+
216
+ .kielceSchedule tr td {
217
+ vertical-align: top;
218
+ padding-right: 10px;
219
+ }
220
+
221
+ .week_column, .date_column {
222
+ white-space: nowrap;
223
+ }
224
+
225
+ .kielceSchedule tr th, .date_column, .topics_column, .notes_column, .reading_column, .milestones_column, .week_end td {
226
+ border-bottom: 1px solid;
227
+ }
228
+ STYLE
229
+ end
230
+
231
+ def timeline_page
232
+ <<PAGE
233
+ <html>
234
+ <head>
235
+ <style>
236
+ #{timeline_style}
237
+ </style>
238
+ </head>
239
+ <body>
240
+ #{timeline_table}
241
+ </body>
242
+ </html>
243
+ PAGE
244
+ end
245
+
246
+ def assignment_style
247
+ <<STYLE
248
+ .kielceAssignmentTable {
249
+ border-spacing: 35px 0;
250
+ }
251
+
252
+ .kielceAssignmentTable tr th {
253
+ text-align: left;
254
+ }
255
+
256
+ .kielceAssignmentTable tr td {
257
+ vertical-align: top;
258
+ }
259
+
260
+ .kielceAssignmentTable_due {
261
+ white-space: nowrap;
262
+ }
263
+
264
+ .kielceAssignmentTable_title {
265
+ white-space: nowrap;
266
+ }
267
+
268
+ .exam {
269
+ background-color: lightgreen;
270
+ }
271
+ STYLE
272
+ end
273
+
274
+
275
+ def assignment_list
276
+ list = <<TABLE
277
+ <table class='kielceAssignmentTable'>
278
+ <tr>
279
+ <th>Due</th>
280
+ <th>Name</th>
281
+ <th>Details</th>
282
+ </tr>
283
+ TABLE
284
+
285
+ by_date = @assignments.values.reject { |item| item.due.nil? || item.type == 'Lab' }.sort_by { |a| a.due }
286
+ by_date.each do |assignment|
287
+ list += ' <tr>'
288
+ list += " <td class='kielceAssignmentTable_due'>#{assignment.due.strftime("%a. %-d %b.")}</td>\n"
289
+ list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
290
+ list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
291
+ list += " </tr>\n\n"
292
+ end
293
+ list += "</table>\n"
294
+ end
295
+
296
+ def lab_list
297
+ list = <<TABLE
298
+ <table class='kielceAssignmentTable'>
299
+ <tr>
300
+ <th>Date</th>
301
+ <th>Name</th>
302
+ <th>Details</th>
303
+ </tr>
304
+ TABLE
305
+ assigned_labs = @assignments.values.select { |item| item.type == 'Lab' && !item.assigned.nil? }
306
+ by_date = assigned_labs.sort { |a, b| a.assigned <=> b.assigned }
307
+ by_date.each do |assignment|
308
+ list += ' <tr>'
309
+ list += " <td class='kielceAssignmentTable_due'>#{assignment.assigned.strftime("%a. %-d %b.")}</td>\n"
310
+ list += " <td class='kielceAssignmentTable_title'>#{assignment.title(:full, true)}</td>\n"
311
+ list += " <td class='kielceAssignmentTable_details'>#{assignment.details}</td>\n"
312
+ list += " </tr>\n\n"
313
+ end
314
+ list += "</table>\n"
315
+ list
316
+ end
317
+ end # end Schedule
318
+ end # module
319
+ end # end KielcePlugins
320
+ #puts Schedule.new(ARGV[0]).timeline_page
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kielce
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Zachary Kurmas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-01-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'An ERB-based templating engine for generating course documents. '
14
+ email:
15
+ executables:
16
+ - kielce
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/kielce
21
+ - lib/kielce.rb
22
+ - lib/kielce/helpers.rb
23
+ - lib/kielce/kielce.rb
24
+ - lib/kielce/kielce_data.rb
25
+ - lib/kielce/kielce_loader.rb
26
+ - lib/kielce_plugins/schedule.rb
27
+ - lib/kielce_plugins/schedule/#schedule.rb#
28
+ - lib/kielce_plugins/schedule/assignment.rb
29
+ - lib/kielce_plugins/schedule/schedule.rb
30
+ homepage: https://github.com/kurmasz/KielceRB
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.0.8
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: An ERB-based templating engine for generating course documents.
53
+ test_files: []