spirit 0.1.0.pre
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.
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/lib/spirit.rb +22 -0
- data/lib/spirit/constants.rb +27 -0
- data/lib/spirit/document.rb +51 -0
- data/lib/spirit/errors.rb +12 -0
- data/lib/spirit/logger.rb +49 -0
- data/lib/spirit/manifest.rb +79 -0
- data/lib/spirit/render.rb +6 -0
- data/lib/spirit/render/errors.rb +11 -0
- data/lib/spirit/render/html.rb +138 -0
- data/lib/spirit/render/sanitize.rb +90 -0
- data/lib/spirit/render/templates.rb +6 -0
- data/lib/spirit/render/templates/header.rb +52 -0
- data/lib/spirit/render/templates/image.rb +54 -0
- data/lib/spirit/render/templates/multi.rb +40 -0
- data/lib/spirit/render/templates/navigation.rb +34 -0
- data/lib/spirit/render/templates/problem.rb +126 -0
- data/lib/spirit/render/templates/short.rb +30 -0
- data/lib/spirit/render/templates/table.rb +109 -0
- data/lib/spirit/render/templates/template.rb +32 -0
- data/lib/spirit/tilt/template.rb +35 -0
- data/lib/spirit/version.rb +4 -0
- data/views/exe.haml +5 -0
- data/views/header.haml +5 -0
- data/views/img.haml +6 -0
- data/views/multi.haml +16 -0
- data/views/nav.haml +5 -0
- data/views/short.haml +12 -0
- data/views/table.haml +28 -0
- metadata +256 -0
@@ -0,0 +1,90 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'sanitize'
|
3
|
+
|
4
|
+
module Spirit
|
5
|
+
|
6
|
+
module Render
|
7
|
+
|
8
|
+
# Encapsulate sanitization options.
|
9
|
+
# @see https://github.com/github/gollum/blob/master/lib/gollum/sanitization.rb
|
10
|
+
class Sanitize < ::Sanitize
|
11
|
+
|
12
|
+
# white-listed elements
|
13
|
+
ELEMENTS = [
|
14
|
+
'a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
|
15
|
+
'blockquote', 'br', 'button', 'caption', 'center', 'cite',
|
16
|
+
'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir',
|
17
|
+
'div', 'dl', 'dt', 'em', 'fieldset', 'font', 'form', 'h1',
|
18
|
+
'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input',
|
19
|
+
'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu',
|
20
|
+
'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp',
|
21
|
+
'select', 'small', 'span', 'strike', 'strong', 'sub',
|
22
|
+
'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th',
|
23
|
+
'thead', 'tr', 'tt', 'u', 'ul', 'var'
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
# white-listed attributes
|
27
|
+
ATTRIBUTES = {
|
28
|
+
'a' => ['href', 'name', 'data-magellan-destination'],
|
29
|
+
'dd' => ['data-magellan-arrival'],
|
30
|
+
'dl' => ['data-magellan-expedition'],
|
31
|
+
'img' => ['src'],
|
32
|
+
:all => ['abbr', 'accept', 'accept-charset',
|
33
|
+
'accesskey', 'action', 'align', 'alt', 'axis',
|
34
|
+
'border', 'cellpadding', 'cellspacing', 'char',
|
35
|
+
'charoff', 'class', 'charset', 'checked', 'cite',
|
36
|
+
'clear', 'cols', 'colspan', 'color',
|
37
|
+
'compact', 'coords', 'datetime', 'dir',
|
38
|
+
'disabled', 'enctype', 'for', 'frame',
|
39
|
+
'headers', 'height', 'hreflang',
|
40
|
+
'hspace', 'id', 'ismap', 'label', 'lang',
|
41
|
+
'longdesc', 'maxlength', 'media', 'method',
|
42
|
+
'multiple', 'name', 'nohref', 'noshade',
|
43
|
+
'nowrap', 'prompt', 'readonly', 'rel', 'rev',
|
44
|
+
'rows', 'rowspan', 'rules', 'scope',
|
45
|
+
'selected', 'shape', 'size', 'span',
|
46
|
+
'start', 'summary', 'tabindex', 'target',
|
47
|
+
'title', 'type', 'usemap', 'valign', 'value',
|
48
|
+
'vspace', 'width']
|
49
|
+
}.freeze
|
50
|
+
|
51
|
+
# white-listed protocols
|
52
|
+
PROTOCOLS = {
|
53
|
+
'a' => {'href' => ['http', 'https', 'mailto', 'ftp', 'irc', 'apt', :relative]},
|
54
|
+
'img' => {'src' => ['http', 'https', :relative]}
|
55
|
+
}.freeze
|
56
|
+
|
57
|
+
# elements to remove (incl. contents)
|
58
|
+
REMOVE_CONTENTS = [
|
59
|
+
'script',
|
60
|
+
'style'
|
61
|
+
].freeze
|
62
|
+
|
63
|
+
# attributes to add to elements
|
64
|
+
ADD_ATTRIBUTES = {
|
65
|
+
'a' => {'rel' => 'nofollow'}
|
66
|
+
}
|
67
|
+
|
68
|
+
# Creates a new sanitizer with {Spirit}'s configuration.
|
69
|
+
def initialize
|
70
|
+
super config
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# @return [Hash] configuration hash.
|
76
|
+
def config
|
77
|
+
{ elements: ELEMENTS.dup,
|
78
|
+
attributes: ATTRIBUTES.dup,
|
79
|
+
protocols: PROTOCOLS.dup,
|
80
|
+
add_attributes: ADD_ATTRIBUTES.dup,
|
81
|
+
remove_contents: REMOVE_CONTENTS.dup,
|
82
|
+
allow_comments: false
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
|
4
|
+
module Spirit
|
5
|
+
|
6
|
+
module Render
|
7
|
+
|
8
|
+
# Keeps track of headers within the same document. It's responsible for
|
9
|
+
# assigning unique names that can be used in the anchors.
|
10
|
+
class Headers
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@headers = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Adds a new header to the set.
|
17
|
+
# @return [Header] header
|
18
|
+
def add(text, level=1)
|
19
|
+
name = text.parameterize
|
20
|
+
if @headers.include? name
|
21
|
+
name += '-%d' % (@headers[name] += 1)
|
22
|
+
else @headers[name] = 0 end
|
23
|
+
Header.new(text, level, name)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
# Renders a header (e.g. +h1+, +h2+, ...) with anchors.
|
29
|
+
class Header < Template
|
30
|
+
|
31
|
+
# Name of template file for rendering headers.
|
32
|
+
TEMPLATE = 'header.haml'
|
33
|
+
|
34
|
+
attr_reader :name
|
35
|
+
|
36
|
+
# Creates a new header.
|
37
|
+
# @param [String] text header text
|
38
|
+
# @param [Fixnum] level 1 to 6
|
39
|
+
# @param [String] name anchor name
|
40
|
+
def initialize(text, level, name)
|
41
|
+
@text, @level, @name = text, level, name
|
42
|
+
end
|
43
|
+
|
44
|
+
def render(locals={})
|
45
|
+
super locals.merge(text: @text, level: @level, name: @name)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Spirit
|
5
|
+
|
6
|
+
module Render
|
7
|
+
|
8
|
+
# Renders a block image with a figure number.
|
9
|
+
class Image < Template
|
10
|
+
|
11
|
+
# <img ...>
|
12
|
+
IMAGE_TAG = 'img'
|
13
|
+
|
14
|
+
# Name of template file for rendering block images
|
15
|
+
TEMPLATE = 'img.haml'
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
# Parses the given text for a block image.
|
20
|
+
def parse(text)
|
21
|
+
Image.new text
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Creates a new image.
|
27
|
+
def initialize(html)
|
28
|
+
@html = html
|
29
|
+
parse_or_raise
|
30
|
+
end
|
31
|
+
|
32
|
+
def render(locals={})
|
33
|
+
super locals.merge(img: @html, caption: @node['alt'])
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Parses the given HTML, or raise {RenderError} if it is invalid.
|
39
|
+
def parse_or_raise
|
40
|
+
frag = Nokogiri::HTML::DocumentFragment.parse(@html)
|
41
|
+
if 1 == frag.children.count and
|
42
|
+
node = frag.children.first and
|
43
|
+
node.is_a? Nokogiri::XML::Element and
|
44
|
+
node.name == IMAGE_TAG
|
45
|
+
@node = node
|
46
|
+
else raise RenderError, 'Not really a block image.'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Spirit
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders multiple choice questions marked up in YAML as HTML.
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# {
|
10
|
+
# "format": "multi",
|
11
|
+
# "question": "How tall is Mount Everest?",
|
12
|
+
# "answer": "A",
|
13
|
+
# "options": {
|
14
|
+
# "A": "452 inches",
|
15
|
+
# "B": "8.85 kilometers"
|
16
|
+
# }
|
17
|
+
# }
|
18
|
+
class Multi < Problem
|
19
|
+
|
20
|
+
# Required key in YAML markup. Associated value should be a dictionary of
|
21
|
+
# label -> choices.
|
22
|
+
OPTIONS = 'options'
|
23
|
+
|
24
|
+
# Name of template file for rendering multiple choice questions.
|
25
|
+
TEMPLATE = 'multi.haml'
|
26
|
+
|
27
|
+
# Checks if the given yaml contains a valid MCQ.
|
28
|
+
# @return [Boolean] true iff the yaml contains a valid MCQ.
|
29
|
+
def valid?
|
30
|
+
super and
|
31
|
+
@yaml[ANSWER].is_a? String and
|
32
|
+
@yaml.has_key?(OPTIONS) and
|
33
|
+
@yaml[OPTIONS].is_a? Hash
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Spirit
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Keeps track of document sections and renders a navigation bar.
|
7
|
+
class Navigation < Template
|
8
|
+
|
9
|
+
# HAML template for navigation bar
|
10
|
+
TEMPLATE = 'nav.haml'
|
11
|
+
|
12
|
+
# Creates a new navigation bar.
|
13
|
+
def initialize
|
14
|
+
@sections = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds a new section.
|
18
|
+
# @param [String] heading section heading
|
19
|
+
# @param [String] name anchor name
|
20
|
+
# @return [void]
|
21
|
+
def append(heading, name)
|
22
|
+
@sections[name] = heading
|
23
|
+
end
|
24
|
+
|
25
|
+
# Renders the navigation bar in HTML.
|
26
|
+
def render(locals={})
|
27
|
+
super locals.merge(sections: @sections)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'spirit/constants'
|
3
|
+
module Spirit
|
4
|
+
|
5
|
+
module Render
|
6
|
+
|
7
|
+
# Renders a single problem. This class doesn't do anything useful; use the
|
8
|
+
# child classes (e.g. {Spirit::Render::Multi}) instead. Child classes should
|
9
|
+
# override {#valid?}.
|
10
|
+
class Problem < Template
|
11
|
+
|
12
|
+
# Required key in YAML markup. Value indicates type of problem.
|
13
|
+
FORMAT = 'format'
|
14
|
+
|
15
|
+
# Required key in YAML markup. Value contains question body.
|
16
|
+
QUESTION = 'question'
|
17
|
+
|
18
|
+
# Required key in YAML markup. Value contains answers.
|
19
|
+
ANSWER = 'answer'
|
20
|
+
|
21
|
+
# Optional key in YAML markup. Value contains problem ID.
|
22
|
+
ID = 'id'
|
23
|
+
|
24
|
+
# Required keys.
|
25
|
+
KEYS = [FORMAT, QUESTION, ANSWER]
|
26
|
+
|
27
|
+
# Stateless markdown renderer.
|
28
|
+
MARKDOWN = ::Redcarpet::Markdown.new(::Redcarpet::Render::HTML, MARKDOWN_EXTENSIONS)
|
29
|
+
|
30
|
+
class << self
|
31
|
+
|
32
|
+
# Parses the given text for questions and answers. If the given text
|
33
|
+
# does not contain valid YAML or does not contain the format key, raises
|
34
|
+
# an {Spirit::Render::RenderError}.
|
35
|
+
# @param [String] text embedded yaml
|
36
|
+
# @return [Problem] problem
|
37
|
+
def parse(text)
|
38
|
+
yaml = YAML.load text
|
39
|
+
get_instance(yaml)
|
40
|
+
rescue ::Psych::SyntaxError => e
|
41
|
+
raise RenderError, e.message
|
42
|
+
end
|
43
|
+
|
44
|
+
# Dynamically creates accessor methods for YAML values.
|
45
|
+
# @example
|
46
|
+
# accessor :id
|
47
|
+
def accessor(*args)
|
48
|
+
args.each { |arg| define_method(arg) { @yaml[arg] } }
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Problem] problem
|
52
|
+
def get_instance(yaml)
|
53
|
+
raise RenderError, "Missing 'format' key in given YAML" unless instantiable? yaml
|
54
|
+
klass = Spirit::Render.const_get(yaml[FORMAT].capitalize)
|
55
|
+
raise NameError unless klass < Problem
|
56
|
+
klass.new(yaml)
|
57
|
+
rescue NameError
|
58
|
+
raise RenderError, 'Unrecognized format: %p' % yaml[FORMAT]
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def instantiable?(yaml)
|
64
|
+
yaml.is_a?(Hash) and yaml.has_key?(FORMAT)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
accessor ID, *KEYS
|
70
|
+
|
71
|
+
# Creates a new problem from the given YAML.
|
72
|
+
# @param [Hash] yaml parsed yaml object
|
73
|
+
def initialize(yaml)
|
74
|
+
@yaml = yaml
|
75
|
+
@yaml[ID] ||= SecureRandom.uuid
|
76
|
+
raise RenderError.new('Invalid problem.') unless @yaml[QUESTION].is_a? String
|
77
|
+
@yaml[QUESTION] = MARKDOWN.render @yaml[QUESTION]
|
78
|
+
end
|
79
|
+
|
80
|
+
# @todo TODO should probably show some error message in the preview,
|
81
|
+
# so that the author doesn't have to read the logs.
|
82
|
+
def render(locals={})
|
83
|
+
raise RenderError.new('Invalid problem.') unless valid?
|
84
|
+
yaml = @yaml.merge(locals)
|
85
|
+
super yaml
|
86
|
+
end
|
87
|
+
|
88
|
+
# Saves the answer to a file on disk.
|
89
|
+
# @todo TODO should probably show some error message in the preview,
|
90
|
+
# so that the author doesn't have to read the logs.
|
91
|
+
def save!
|
92
|
+
raise RenderError.new('Invalid problem.') unless valid?
|
93
|
+
solution = File.join(Spirit::SOLUTION_DIR, id + Spirit::SOLUTION_EXT)
|
94
|
+
File.open(solution, 'wb+') { |file| Marshal.dump answer, file }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Retrieves the answer from the given YAML object in a serializable form.
|
98
|
+
# @see #serializable
|
99
|
+
# @return [String, Numeric, TrueClass, FalseClass] answer
|
100
|
+
def answer
|
101
|
+
serialize @yaml[ANSWER]
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# If +obj+ is one of String, Numeric, +true+, or +false+, the it's
|
107
|
+
# returned. Otherwise, +to_s+ is invoked on the object and returned.
|
108
|
+
# @return [String, Numeric, TrueClass, FalseClass] answer
|
109
|
+
def serialize(obj)
|
110
|
+
case obj
|
111
|
+
when String, Numeric, TrueClass, FalseClass then obj
|
112
|
+
else obj.to_s end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Checks that all required {KEYS} exist in the YAML, and that the
|
116
|
+
# question is given as a string.
|
117
|
+
# @return [Boolean] true iff the parsed yaml contains a valid problem.
|
118
|
+
def valid?
|
119
|
+
KEYS.all? { |key| @yaml.has_key? key } and question.is_a? String
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Spirit
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders short questions marked up in YAML as HTML.
|
7
|
+
# @example
|
8
|
+
# {
|
9
|
+
# "format": "short",
|
10
|
+
# "question": "What is the most commonly used word in English?",
|
11
|
+
# "answer": "the"
|
12
|
+
# }
|
13
|
+
class Short < Problem
|
14
|
+
|
15
|
+
# Name of template file for rendering short answer questions.
|
16
|
+
TEMPLATE = 'short.haml'
|
17
|
+
|
18
|
+
# Checks if the given yaml contains a valid MCQ.
|
19
|
+
# @return [Boolean] true iff the yaml contains a valid MCQ.
|
20
|
+
def valid?
|
21
|
+
super and
|
22
|
+
not @yaml[ANSWER].nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Spirit
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders table problems marked up in YAML as HTML.
|
7
|
+
#
|
8
|
+
# The grid should be given as a 2-dimensional array that represents the
|
9
|
+
# table to be filled in. +"?"+ is a special token used in the grid to
|
10
|
+
# indicate cells that require student input.
|
11
|
+
#
|
12
|
+
# The answer should also be given as a 2-dimensional array. However, a
|
13
|
+
# dummy token may be used in cells that do not require student input to
|
14
|
+
# cut redundancy. In the example below, the +"-"+ token is used.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# {
|
18
|
+
# "format": "table",
|
19
|
+
# "question": "fill me in",
|
20
|
+
# "grid": [[0, "?", 2], [3, "?", 5]],
|
21
|
+
# "answer": [["-", 1, "-"], ["-", 4, "-"]
|
22
|
+
# }
|
23
|
+
class Table < Problem
|
24
|
+
|
25
|
+
# Name of template file for rendering table problems.
|
26
|
+
TEMPLATE = 'table.haml'
|
27
|
+
|
28
|
+
# Optional headings key.
|
29
|
+
HEADINGS = 'headings'
|
30
|
+
|
31
|
+
# Required grid key.
|
32
|
+
GRID = 'grid'
|
33
|
+
|
34
|
+
# Special token indicating that the cell should be filled in.
|
35
|
+
FILL_ME_IN = '?'
|
36
|
+
|
37
|
+
accessor HEADINGS, GRID
|
38
|
+
|
39
|
+
# Ensures that the +headings+ key exists.
|
40
|
+
def initialize(yaml)
|
41
|
+
yaml[HEADINGS] ||= nil
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
# Checks if the given yaml contains a valid table.
|
46
|
+
# @return [Boolean] true iff the yaml contains a valid table.
|
47
|
+
def valid?
|
48
|
+
super and
|
49
|
+
valid_grid? and
|
50
|
+
valid_answer?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Gets the expected answer, in www-form-urlencoded format.
|
54
|
+
# @return [Hash] answers, as expected from student's form submission
|
55
|
+
def answer
|
56
|
+
return @answer unless @answer.nil?
|
57
|
+
@answer = encode_answer
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean] true iff the given cell is an input cell
|
61
|
+
def self.input?(cell)
|
62
|
+
cell == FILL_ME_IN
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Iterates through each cell in the provided grid to look for answers
|
68
|
+
# cells that require input and takes the answer from the answers array.
|
69
|
+
# For example, if the answer for a cell at [0][1] is 6, the returned
|
70
|
+
# hash will contain
|
71
|
+
#
|
72
|
+
# {'0' => {'1' => 6}}
|
73
|
+
#
|
74
|
+
# @return [Hash] answers
|
75
|
+
def encode_answer
|
76
|
+
encoded, ans = {}, @yaml[ANSWER]
|
77
|
+
grid.each_with_index do |row, i|
|
78
|
+
row.each_with_index do |cell, j|
|
79
|
+
next unless Table.input? cell
|
80
|
+
encoded[i.to_s] ||= {}
|
81
|
+
encoded[i.to_s][j.to_s] = serialize ans[i][j]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
encoded
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Boolean] true iff the yaml contains a valid grid.
|
88
|
+
def valid_grid?
|
89
|
+
@yaml.has_key? GRID and is_2d_array? grid
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Boolean] true iff +answer+ is a valid 2D array and has the
|
93
|
+
# same number of rows as +grid+.
|
94
|
+
def valid_answer?
|
95
|
+
ans = @yaml[ANSWER]
|
96
|
+
is_2d_array? ans and ans.size == grid.size
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Boolean] true iff +t+ is a 2-dimensional array.
|
100
|
+
def is_2d_array?(t)
|
101
|
+
t.is_a? Array and t.all? { |row| row.is_a? Array }
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|