aladdin 0.0.1 → 0.0.3

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