spirit 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|