aladdin 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +16 -10
- data/assets/favicon.ico +0 -0
- data/assets/images/graphic.png +0 -0
- data/assets/images/no_gravatar.gif +0 -0
- data/assets/javascripts/app.js +76 -0
- data/bin/aladdin +2 -17
- data/lib/aladdin.rb +58 -13
- data/lib/aladdin/app.rb +27 -7
- data/lib/aladdin/cli.rb +29 -0
- data/lib/aladdin/commands/new.rb +16 -0
- data/lib/aladdin/commands/server.rb +10 -0
- data/lib/aladdin/mixin/logger.rb +26 -0
- data/lib/aladdin/mixin/weak_comparator.rb +60 -0
- data/lib/aladdin/render/error.rb +20 -0
- data/lib/aladdin/render/image.rb +54 -0
- data/lib/aladdin/render/markdown.rb +111 -8
- data/lib/aladdin/render/multi.rb +40 -0
- data/lib/aladdin/render/navigation.rb +34 -0
- data/lib/aladdin/render/problem.rb +114 -0
- data/lib/aladdin/render/sanitize.rb +3 -1
- data/lib/aladdin/render/short.rb +30 -0
- data/lib/aladdin/render/table.rb +109 -0
- data/lib/aladdin/render/template.rb +32 -0
- data/lib/aladdin/submission.rb +92 -0
- data/lib/aladdin/version.rb +1 -1
- data/skeleton/images/graphic.png +0 -0
- data/skeleton/index.md +3 -0
- data/views/haml/exe.haml +5 -0
- data/views/haml/img.haml +6 -0
- data/views/haml/layout.haml +44 -12
- data/views/haml/multi.haml +18 -0
- data/views/haml/nav.haml +5 -0
- data/views/haml/short.haml +14 -0
- data/views/haml/table.haml +30 -0
- data/views/scss/app.scss +50 -44
- data/views/scss/mathjax.scss +5 -0
- data/views/scss/pygment.scss +9 -6
- metadata +47 -18
- data/assets/images/foundation/orbit/bullets.jpg +0 -0
- data/assets/images/foundation/orbit/left-arrow-small.png +0 -0
- data/assets/images/foundation/orbit/left-arrow.png +0 -0
- data/assets/images/foundation/orbit/loading.gif +0 -0
- data/assets/images/foundation/orbit/mask-black.png +0 -0
- data/assets/images/foundation/orbit/pause-black.png +0 -0
- data/assets/images/foundation/orbit/right-arrow-small.png +0 -0
- data/assets/images/foundation/orbit/right-arrow.png +0 -0
- data/assets/images/foundation/orbit/rotator-black.png +0 -0
- data/assets/images/foundation/orbit/timer-black.png +0 -0
- data/views/haml/index.haml +0 -43
@@ -0,0 +1,20 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Aladdin
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# The base exception for {Aladdin::Render} Errors
|
7
|
+
class Error < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
# This exception is raised if a parse error occurs.
|
11
|
+
class ParseError < Error
|
12
|
+
end
|
13
|
+
|
14
|
+
# This exception is raised if a render error occurs.
|
15
|
+
class RenderError < Error
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Aladdin
|
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 {ParseError} 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 ParseError.new 'Not really a block image.'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -1,37 +1,140 @@
|
|
1
|
+
require 'aladdin/render/sanitize'
|
2
|
+
require 'aladdin/render/error'
|
3
|
+
require 'aladdin/render/template'
|
4
|
+
require 'aladdin/render/image'
|
5
|
+
require 'aladdin/render/problem'
|
6
|
+
require 'aladdin/render/multi'
|
7
|
+
require 'aladdin/render/short'
|
8
|
+
require 'aladdin/render/table'
|
9
|
+
require 'aladdin/render/navigation'
|
10
|
+
|
1
11
|
# ~*~ encoding: utf-8 ~*~
|
2
12
|
module Aladdin
|
3
13
|
|
4
|
-
#
|
14
|
+
# aladdin-render module for all of Laddin's rendering needs.
|
5
15
|
module Render
|
6
16
|
|
7
17
|
# HTML Renderer for Markdown.
|
18
|
+
#
|
8
19
|
# It creates pygmentized code blocks, supports hard-wraps, and only
|
9
|
-
# generates links for protocols which are considered safe.
|
20
|
+
# generates links for protocols which are considered safe. Adds support for
|
21
|
+
# embedded JSON, which are used to markup quizzes and tables. Refer to
|
22
|
+
# {CONFIGURATION} for more details.
|
23
|
+
#
|
10
24
|
# @see http://github.github.com/github-flavored-markdown/
|
11
25
|
class HTML < ::Redcarpet::Render::HTML
|
26
|
+
include Aladdin::Mixin::Logger
|
12
27
|
|
13
28
|
@sanitize = Aladdin::Sanitize.new
|
14
|
-
|
29
|
+
@entities = HTMLEntities.new
|
30
|
+
|
31
|
+
class << self; attr_reader :sanitize, :entities; end
|
32
|
+
|
33
|
+
# Paragraphs that start and end with braces are treated as JSON blocks
|
34
|
+
# and are parsed for questions/answers.
|
35
|
+
PROBLEM_REGEX = %r<^\s*{.+$>
|
36
|
+
|
37
|
+
# Paragraphs that only contain images are rendered differently.
|
38
|
+
IMAGE_REGEX = %r{^\s*<img[^<>]+>\s*$}
|
39
|
+
|
40
|
+
# Renderer configuration options.
|
41
|
+
CONFIGURATION = {
|
42
|
+
hard_wrap: true,
|
43
|
+
safe_links_only: true,
|
44
|
+
}
|
15
45
|
|
16
46
|
# Creates a new HTML renderer.
|
17
47
|
# @param [Hash] options described in the RedCarpet documentation.
|
18
48
|
def initialize(options = {})
|
19
|
-
super options.merge(
|
49
|
+
super options.merge(CONFIGURATION)
|
50
|
+
exe_template = File.join(Aladdin::VIEWS[:haml], 'exe.haml')
|
51
|
+
@exe, @nav = Haml::Engine.new(File.read exe_template), Navigation.new
|
52
|
+
@prob, @img = 0, 0 # indices for Problem # and Figure #
|
20
53
|
end
|
21
54
|
|
22
55
|
# Pygmentizes code blocks.
|
23
56
|
# @param [String] code code block contents
|
24
|
-
# @param [String]
|
57
|
+
# @param [String] marker name of language, for syntax highlighting
|
25
58
|
# @return [String] highlighted code
|
26
|
-
def block_code(code,
|
27
|
-
|
59
|
+
def block_code(code, marker)
|
60
|
+
language, type, id = (marker || 'text').split ':'
|
61
|
+
highlighted = Albino.colorize code, language
|
62
|
+
case type
|
63
|
+
when 'demo', 'test'
|
64
|
+
executable id: id, raw: code, colored: highlighted
|
65
|
+
else highlighted end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Detects problem blocks and image blocks.
|
69
|
+
# @param [String] text paragraph text
|
70
|
+
def paragraph(text)
|
71
|
+
case text
|
72
|
+
when PROBLEM_REGEX then problem(text)
|
73
|
+
when IMAGE_REGEX then block_image(text)
|
74
|
+
else p(text) end
|
75
|
+
rescue Error => e # fall back to paragraph
|
76
|
+
logger.warn e.message
|
77
|
+
p(text)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Increases all header levels by one and keeps track of document
|
81
|
+
# sections.
|
82
|
+
def header(text, level)
|
83
|
+
level += 1
|
84
|
+
html = h(text, level)
|
85
|
+
if level == 2
|
86
|
+
index = @nav << text
|
87
|
+
html += "<a name='section_#{index}' data-magellan-destination='section_#{index}'/>"
|
88
|
+
end
|
89
|
+
html
|
28
90
|
end
|
29
91
|
|
30
92
|
# Sanitizes the final document.
|
31
93
|
# @param [String] document html document
|
32
94
|
# @return [String] sanitized document
|
33
95
|
def postprocess(document)
|
34
|
-
HTML.sanitize.clean document
|
96
|
+
HTML.sanitize.clean(@nav.render + document.force_encoding('utf-8'))
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Prepares an executable code block.
|
102
|
+
# @option opts [String] id author-supplied ID
|
103
|
+
# @option opts [String] raw code to execute
|
104
|
+
# @option opts [String] colored syntax highlighted code
|
105
|
+
# @return [String]
|
106
|
+
def executable(opts)
|
107
|
+
opts[:colored] + @exe.render(Object.new, id: opts[:id], raw: opts[:raw])
|
108
|
+
end
|
109
|
+
|
110
|
+
# Prepares a problem form. Raises {RenderError} or {ParseError} if the
|
111
|
+
# given text does not contain valid json markup for a problem.
|
112
|
+
# @param [String] json JSON markup
|
113
|
+
# @return [String] rendered HTML
|
114
|
+
def problem(json)
|
115
|
+
b = '\\' # unescape backslashes
|
116
|
+
problem = Problem.parse(HTML.entities.decode(json).gsub(b, b * 4))
|
117
|
+
problem.save! and problem.render(index: @prob += 1)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Prepares a block image. Raises {RenderError} or {ParseError} if the
|
121
|
+
# given text does not contain a valid image block.
|
122
|
+
def block_image(text)
|
123
|
+
image = Image.parse(text)
|
124
|
+
image.render(index: @img += 1)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Wraps the given text with header tags.
|
128
|
+
# @return [String] wrapped text
|
129
|
+
def h(text, level)
|
130
|
+
"<h#{level}>#{text}</h#{level}>"
|
131
|
+
end
|
132
|
+
|
133
|
+
# Wraps the given text with paragraph tags.
|
134
|
+
# @param [String] text paragraph text
|
135
|
+
# @return [String] wrapped text
|
136
|
+
def p(text)
|
137
|
+
'<p>' + text + '</p>'
|
35
138
|
end
|
36
139
|
|
37
140
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Aladdin
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders multiple choice questions marked up in JSON 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 JSON 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 json contains a valid MCQ.
|
28
|
+
# @return [Boolean] true iff the json contains a valid MCQ.
|
29
|
+
def valid?
|
30
|
+
super and
|
31
|
+
@json[ANSWER].is_a? String and
|
32
|
+
@json.has_key?(OPTIONS) and
|
33
|
+
@json[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 Aladdin
|
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
|
+
# @return [Fixnum] section index
|
20
|
+
def <<(heading)
|
21
|
+
@sections << heading
|
22
|
+
@sections.size - 1
|
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,114 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Aladdin
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders a single problem. This class doesn't do anything useful; use the
|
7
|
+
# child classes (e.g. {Aladdin::Render::Multi}) instead. Child classes should
|
8
|
+
# override {#valid?}.
|
9
|
+
class Problem < Template
|
10
|
+
|
11
|
+
# Required key in JSON markup. Value indicates type of problem.
|
12
|
+
FORMAT = 'format'
|
13
|
+
|
14
|
+
# Required key in JSON markup. Value contains question body.
|
15
|
+
QUESTION = 'question'
|
16
|
+
|
17
|
+
# Required key in JSON markup. Value contains answers.
|
18
|
+
ANSWER = 'answer'
|
19
|
+
|
20
|
+
# Optional key in JSON markup. Value contains problem ID.
|
21
|
+
ID = 'id'
|
22
|
+
|
23
|
+
# Required keys.
|
24
|
+
KEYS = [FORMAT, QUESTION, ANSWER]
|
25
|
+
|
26
|
+
class << self
|
27
|
+
|
28
|
+
# Parses the given text for questions and answers. If the given text
|
29
|
+
# does not contain valid JSON or does not contain the format key, raises
|
30
|
+
# an {Aladdin::Render::ParseError}.
|
31
|
+
# @param [String] text markdown text
|
32
|
+
def parse(text)
|
33
|
+
json = JSON.parse text
|
34
|
+
if json.is_a?(Hash) and json.has_key?(FORMAT)
|
35
|
+
get_instance(json)
|
36
|
+
else raise ParseError.new("Expected a JSON object containing the #{FORMAT} key.")
|
37
|
+
end
|
38
|
+
rescue JSON::JSONError => e
|
39
|
+
raise ParseError.new(e.message)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Dynamically creates accessor methods for JSON values.
|
43
|
+
# @example
|
44
|
+
# accessor :id
|
45
|
+
def accessor(*args)
|
46
|
+
args.each { |arg| define_method(arg) { @json[arg] } }
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Problem] problem
|
50
|
+
def get_instance(json)
|
51
|
+
klass = Aladdin::Render.const_get(json[FORMAT].capitalize)
|
52
|
+
raise NameError.new unless klass < Problem
|
53
|
+
klass.new(json)
|
54
|
+
rescue NameError
|
55
|
+
raise ParseError.new('Unrecognized format: %p' % json[FORMAT])
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
accessor ID, *KEYS
|
61
|
+
|
62
|
+
# Creates a new problem from the given JSON.
|
63
|
+
# @param [Hash] json parsed JSON object
|
64
|
+
def initialize(json)
|
65
|
+
@json = json
|
66
|
+
@json[ID] ||= SecureRandom.uuid
|
67
|
+
end
|
68
|
+
|
69
|
+
# @todo TODO should probably show some error message in the preview,
|
70
|
+
# so that the author doesn't have to read the logs.
|
71
|
+
def render(locals={})
|
72
|
+
raise RenderError.new('Invalid problem.') unless valid?
|
73
|
+
super @json.merge(locals)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Saves the answer to a file on disk.
|
77
|
+
# @todo TODO should probably show some error message in the preview,
|
78
|
+
# so that the author doesn't have to read the logs.
|
79
|
+
def save!
|
80
|
+
raise RenderError.new('Invalid problem.') unless valid?
|
81
|
+
solution = File.join(Aladdin::DATA_DIR, id + Aladdin::DATA_EXT)
|
82
|
+
File.open(solution, 'wb+') { |file| Marshal.dump answer, file }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Retrieves the answer from the given JSON object in a serializable form.
|
86
|
+
# @see #serializable
|
87
|
+
# @return [String, Numeric, TrueClass, FalseClass] answer
|
88
|
+
def answer
|
89
|
+
serialize @json[ANSWER]
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# If +obj+ is one of String, Numeric, +true+, or +false+, the it's
|
95
|
+
# returned. Otherwise, +to_s+ is invoked on the object and returned.
|
96
|
+
# @return [String, Numeric, TrueClass, FalseClass] answer
|
97
|
+
def serialize(obj)
|
98
|
+
case obj
|
99
|
+
when String, Numeric, TrueClass, FalseClass then obj
|
100
|
+
else obj.to_s end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Checks that all required {KEYS} exist in the JSON, and that the
|
104
|
+
# question is given as a string.
|
105
|
+
# @return [Boolean] true iff the parsed json contains a valid problem.
|
106
|
+
def valid?
|
107
|
+
KEYS.all? { |key| @json.has_key? key } and question.is_a? String
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -23,7 +23,9 @@ module Aladdin
|
|
23
23
|
|
24
24
|
# white-listed attributes
|
25
25
|
ATTRIBUTES = {
|
26
|
-
'a' => ['href'],
|
26
|
+
'a' => ['href', 'name', 'data-magellan-destination'],
|
27
|
+
'dd' => ['data-magellan-arrival'],
|
28
|
+
'dl' => ['data-magellan-expedition'],
|
27
29
|
'img' => ['src'],
|
28
30
|
:all => ['abbr', 'accept', 'accept-charset',
|
29
31
|
'accesskey', 'action', 'align', 'alt', 'axis',
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# ~*~ encoding: utf-8 ~*~
|
2
|
+
module Aladdin
|
3
|
+
|
4
|
+
module Render
|
5
|
+
|
6
|
+
# Renders short questions marked up in JSON 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 json contains a valid MCQ.
|
19
|
+
# @return [Boolean] true iff the json contains a valid MCQ.
|
20
|
+
def valid?
|
21
|
+
super and
|
22
|
+
not @json[ANSWER].nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|