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.
Files changed (49) hide show
  1. data/README.md +16 -10
  2. data/assets/favicon.ico +0 -0
  3. data/assets/images/graphic.png +0 -0
  4. data/assets/images/no_gravatar.gif +0 -0
  5. data/assets/javascripts/app.js +76 -0
  6. data/bin/aladdin +2 -17
  7. data/lib/aladdin.rb +58 -13
  8. data/lib/aladdin/app.rb +27 -7
  9. data/lib/aladdin/cli.rb +29 -0
  10. data/lib/aladdin/commands/new.rb +16 -0
  11. data/lib/aladdin/commands/server.rb +10 -0
  12. data/lib/aladdin/mixin/logger.rb +26 -0
  13. data/lib/aladdin/mixin/weak_comparator.rb +60 -0
  14. data/lib/aladdin/render/error.rb +20 -0
  15. data/lib/aladdin/render/image.rb +54 -0
  16. data/lib/aladdin/render/markdown.rb +111 -8
  17. data/lib/aladdin/render/multi.rb +40 -0
  18. data/lib/aladdin/render/navigation.rb +34 -0
  19. data/lib/aladdin/render/problem.rb +114 -0
  20. data/lib/aladdin/render/sanitize.rb +3 -1
  21. data/lib/aladdin/render/short.rb +30 -0
  22. data/lib/aladdin/render/table.rb +109 -0
  23. data/lib/aladdin/render/template.rb +32 -0
  24. data/lib/aladdin/submission.rb +92 -0
  25. data/lib/aladdin/version.rb +1 -1
  26. data/skeleton/images/graphic.png +0 -0
  27. data/skeleton/index.md +3 -0
  28. data/views/haml/exe.haml +5 -0
  29. data/views/haml/img.haml +6 -0
  30. data/views/haml/layout.haml +44 -12
  31. data/views/haml/multi.haml +18 -0
  32. data/views/haml/nav.haml +5 -0
  33. data/views/haml/short.haml +14 -0
  34. data/views/haml/table.haml +30 -0
  35. data/views/scss/app.scss +50 -44
  36. data/views/scss/mathjax.scss +5 -0
  37. data/views/scss/pygment.scss +9 -6
  38. metadata +47 -18
  39. data/assets/images/foundation/orbit/bullets.jpg +0 -0
  40. data/assets/images/foundation/orbit/left-arrow-small.png +0 -0
  41. data/assets/images/foundation/orbit/left-arrow.png +0 -0
  42. data/assets/images/foundation/orbit/loading.gif +0 -0
  43. data/assets/images/foundation/orbit/mask-black.png +0 -0
  44. data/assets/images/foundation/orbit/pause-black.png +0 -0
  45. data/assets/images/foundation/orbit/right-arrow-small.png +0 -0
  46. data/assets/images/foundation/orbit/right-arrow.png +0 -0
  47. data/assets/images/foundation/orbit/rotator-black.png +0 -0
  48. data/assets/images/foundation/orbit/timer-black.png +0 -0
  49. 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
- # laddin-render module for all of Laddin's rendering needs.
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
- class << self; attr_reader :sanitize; end
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(hard_wrap: true, safe_links_only: true)
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] language name of language, for syntax highlighting
57
+ # @param [String] marker name of language, for syntax highlighting
25
58
  # @return [String] highlighted code
26
- def block_code(code, language)
27
- Albino.colorize code, language
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
+