kielce 2.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.
@@ -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: []