aladdin 0.0.1 → 0.0.3
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/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
|
+
|