spirit 0.1.0.pre

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012-2013 Jiunn Haur Lim, Carnegie Mellon University
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Spirit
2
+
3
+ Genie's parser, which parses the Genie Markup Language and produces HTML
4
+ partials. Both Aladdin and Lamp should depend on this gem for all their parsing
5
+ needs.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'spirit'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install spirit
20
+
21
+ ## Usage
22
+
23
+ ### Parsing a document
24
+
25
+ Spirit::Document.new(data, opts).render //=> rendered html
26
+
27
+ ### Parsing a manifest
28
+
29
+ Spirit::Manifest.new(data, opts) //=> configuration object
@@ -0,0 +1,22 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'spirit/logger'
3
+ require 'spirit/constants'
4
+ require 'spirit/errors'
5
+ require 'spirit/document'
6
+ require 'spirit/manifest'
7
+
8
+ module Spirit
9
+ extend self
10
+
11
+ attr_reader :logger
12
+
13
+ # Initializes the logger. It takes the same arguments as +Logger::new+.
14
+ # Invoke this at the beginning if you wish {Spirit} to log to another
15
+ # location than +STDOUT+.
16
+ def initialize_logger(output=STDOUT, *args)
17
+ @logger = Logger.new output, *args
18
+ end
19
+
20
+ initialize_logger
21
+
22
+ end
@@ -0,0 +1,27 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'tmpdir'
3
+
4
+ module Spirit
5
+
6
+ # Path to templates
7
+ VIEWS = File.join File.dirname(__FILE__), *%w(.. .. views)
8
+
9
+ # Markdown extensions for Redcarpet
10
+ MARKDOWN_EXTENSIONS = {
11
+ no_intra_emphasis: true,
12
+ tables: true,
13
+ fenced_code_blocks: true,
14
+ autolink: true,
15
+ strikethrough: true,
16
+ }
17
+
18
+ SOLUTION_DIR = Dir.tmpdir
19
+ SOLUTION_EXT = '.sol'
20
+
21
+ # Name of index page.
22
+ INDEX = 'index.md'
23
+
24
+ # Name of manifest file.
25
+ MANIFEST = 'manifest.yml'
26
+
27
+ end
@@ -0,0 +1,51 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'spirit/constants'
3
+ require 'spirit/errors'
4
+ require 'spirit/render'
5
+
6
+ module Spirit
7
+
8
+ # Document written in Genie Markup Language.
9
+ # @todo TODO clean?
10
+ class Document
11
+
12
+ attr_accessor :data, :engine
13
+
14
+ # Creates a new document from the given source. It should contain valid
15
+ # markdown + embedded YAML.
16
+ # @param [#read, #to_str] source
17
+ # @param [Hash] opts options for {::Redcarpet}
18
+ def initialize(source, opts={})
19
+ opts = MARKDOWN_EXTENSIONS.merge opts
20
+ rndr = Render::HTML.new
21
+ @engine = ::Redcarpet::Markdown.new(rndr, opts)
22
+ @data = case
23
+ when source.respond_to?(:to_str) then source.to_str
24
+ when source.respond_to?(:read) then source.read
25
+ else nil end
26
+ end
27
+
28
+ # @return [Boolean] true iff if was a clean parse with no errors.
29
+ def clean?
30
+ # TODO
31
+ end
32
+
33
+ # Rendered output is returned as a string if +anIO+ is not provided. The
34
+ # output is sanitized with {Spirit::Render::Sanitize}, and should be
35
+ # considered safe for embedding into a HTML page without further escaping or
36
+ # sanitization.
37
+ #
38
+ # @param [IO] anIO if given, the HTML partial will be written to it.
39
+ # @return [String] if +anIO+ was not provided, returns the HTML string.
40
+ # @return [Fixnum] if +anIO+ was provided, returns the number of bytes
41
+ # written.
42
+ # @raise [Spirit::Error] if a fatal error is encountered.
43
+ def render(anIO=nil)
44
+ output = engine.render(data)
45
+ return anIO.write(output) unless anIO.nil?
46
+ output
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,12 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ module Spirit
3
+
4
+ # The base exception for all {Spirit} errors. Raised if the parser is unable
5
+ # to continue.
6
+ class Error < StandardError; end
7
+
8
+ class DocumentError < Error; end
9
+
10
+ class ManifestError < Error; end
11
+
12
+ end
@@ -0,0 +1,49 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'active_support/core_ext/logger'
3
+
4
+ module Spirit
5
+
6
+ # @see https://github.com/chriseppstein/compass/blob/stable/lib/compass/logger.rb
7
+ class Logger < ::Logger
8
+
9
+ COLORS = { clear: 0, red: 31, green: 32, blue: 35, yellow: 33, grey: 37 }
10
+
11
+ ACTION_COLORS = {
12
+ :error => :red,
13
+ :warning => :yellow,
14
+ :problem => :blue,
15
+ }
16
+
17
+ # Record that an action has occurred.
18
+ def record(action, *args)
19
+ msg = ''
20
+ msg << color(ACTION_COLORS[action])
21
+ msg << action_padding(action) + action.to_s
22
+ msg << color(:clear)
23
+ msg << ' ' + args.join(' ')
24
+ info msg
25
+ end
26
+
27
+ private
28
+
29
+ def color(c)
30
+ (c and code = COLORS[c.to_sym]) ? "\e[#{code}m" : ''
31
+ end
32
+
33
+ # Adds padding to the left of an action that was performed.
34
+ def action_padding(action)
35
+ ' ' * [(max_action_length - action.to_s.length), 0].max
36
+ end
37
+
38
+ # the maximum length of all the actions known to the logger.
39
+ def max_action_length
40
+ @max_action_length ||= actions.reduce(0) { |m, a| [m, a.to_s.length].max }
41
+ end
42
+
43
+ def actions
44
+ @actions ||= ACTION_COLORS.keys
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,79 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'active_support/core_ext/hash'
3
+ require 'active_support/core_ext/string'
4
+
5
+ module Spirit
6
+
7
+ # Manifest described in YAML. Values in this hash should not be trusted,
8
+ # since they are provided by the user.
9
+ class Manifest < Hash
10
+
11
+ # Expected types.
12
+ TYPES = {
13
+ verify: { 'bin' => 'string', 'arg_prefix' => 'string' },
14
+ title: 'string',
15
+ description: 'string',
16
+ categories: %w(string),
17
+ static_paths: %w(string)
18
+ }
19
+
20
+ # Creates a new configuration hash from the given source.
21
+ def initialize(hash)
22
+ super nil
23
+ hash ||= {}
24
+ bad_file(hash.class) unless hash.is_a? Hash
25
+ merge! hash.symbolize_keys
26
+ check_types
27
+ end
28
+
29
+ # Load configuration from given string.
30
+ # @param [String] source
31
+ # @return [Manifest] manifest
32
+ def self.load(source)
33
+ new YAML.load source
34
+ rescue ::Psych::SyntaxError => e
35
+ raise ManifestError, e.message
36
+ end
37
+
38
+ # Load configuration from given yaml file.
39
+ # @param [String] path
40
+ # @return [Manifest] manifest
41
+ def self.load_file(path)
42
+ File.open(path, 'r:utf-8') { |f| new YAML.load f.read }
43
+ rescue ::Psych::SyntaxError => e
44
+ raise ManifestError, e.message
45
+ end
46
+
47
+ private_class_method :new
48
+
49
+ private
50
+
51
+ # Checks that the given hash has the valid types for each value, if they
52
+ # exist.
53
+ # @raise [ManifestError] if a bad type is encountered.
54
+ def check_types(key='root', expected=TYPES, actual=self, opts={})
55
+ bad_type(key, expected, actual, opts) unless actual.is_a? expected.class
56
+ case actual
57
+ when Hash then actual.each { |k, v| check_types(k, expected[k], v) }
58
+ when Enumerable then actual.each { |v| check_types(key, expected.first, v, enum: true) }
59
+ end
60
+ end
61
+
62
+ def bad_file(type)
63
+ raise ManifestError, <<-eos.squish
64
+ The manifest file should contain a dictionary, but a #{type.name} was
65
+ found.
66
+ eos
67
+ end
68
+
69
+ def bad_type(key, expected, actual, opts={})
70
+ verb = opts[:enum] ? 'contain' : 'be'
71
+ raise ManifestError, <<-eos.squish
72
+ The #{key} option in the manifest file should #{verb} a #{expected.class.name}
73
+ instead of a #{actual.class.name}.
74
+ eos
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,6 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'albino'
3
+ require 'haml'
4
+ require 'redcarpet'
5
+
6
+ require 'spirit/render/html'
@@ -0,0 +1,11 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ module Spirit
3
+
4
+ module Render
5
+
6
+ # The base exception for {Spirit::Render} errors.
7
+ class RenderError < DocumentError; end
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,138 @@
1
+ # ~*~ encoding: utf-8 ~*~
2
+ require 'spirit/render/errors'
3
+ require 'spirit/render/sanitize'
4
+ require 'spirit/render/templates'
5
+
6
+ module Spirit
7
+
8
+ module Render
9
+
10
+ # HTML Renderer for Genie Markup Language, which is just GitHub Flavored
11
+ # Markdown with Embedded YAML for describing problems. Designed for use
12
+ # with Redcarpet.
13
+ # @see Spirit::Tilt::Template
14
+ # @see http://github.github.com/github-flavored-markdown/
15
+ class HTML < ::Redcarpet::Render::HTML
16
+
17
+ @sanitize = Sanitize.new
18
+ class << self; attr_reader :sanitize end
19
+
20
+ # Paragraphs that start and end with '---' are treated as embedded YAML
21
+ # and are parsed for questions/answers.
22
+ PROBLEM_REGEX = /^"""$(.*?)^"""$/m
23
+
24
+ # Paragraphs that only contain images are rendered with {Spirit::Render::Image}.
25
+ IMAGE_REGEX = /\A\s*<img[^<>]+>\s*\z/m
26
+
27
+ # Renderer configuration options.
28
+ CONFIGURATION = {
29
+ hard_wrap: true,
30
+ no_styles: true,
31
+ }
32
+
33
+ # Creates a new HTML renderer.
34
+ # @param [Hash] options described in the RedCarpet documentation.
35
+ def initialize(options={})
36
+ super CONFIGURATION.merge options
37
+ @nav, @headers = Navigation.new, Headers.new
38
+ @prob, @img = 0, 0 # indices for Problem #, Figure #
39
+ end
40
+
41
+ # Pygmentizes code blocks.
42
+ # @param [String] code code block contents
43
+ # @param [String] marker name of language, for syntax highlighting
44
+ # @return [String] highlighted code
45
+ def block_code(code, marker)
46
+ #language, type, id = (marker || 'text').split ':'
47
+ #highlighted = Albino.colorize code, language
48
+ language, _, _ = (marker || 'text').split ':'
49
+ Albino.colorize code, language
50
+ # TODO
51
+ #case type
52
+ #when 'demo', 'test'
53
+ # executable id: id, raw: code, colored: highlighted
54
+ #else highlighted end
55
+ end
56
+
57
+ # Detects block images and renders them as such.
58
+ # @return [String] rendered html
59
+ def paragraph(text)
60
+ case text
61
+ when IMAGE_REGEX then block_image(text)
62
+ else p(text) end
63
+ rescue RenderError => e # fall back to paragraph
64
+ Spirit.logger.warn e.message
65
+ p(text)
66
+ end
67
+
68
+ # Increases all header levels by one and keeps a navigation bar.
69
+ # @return [String] rendered html
70
+ def header(text, level)
71
+ html, name = h(text, level += 1)
72
+ @nav.append(text, name) if level == 2
73
+ html
74
+ end
75
+
76
+ # Runs a first pass through the document to look for problem blocks.
77
+ # @param [String] document markdown document
78
+ def preprocess(document)
79
+ document.gsub(PROBLEM_REGEX) { |yaml| problem $1 }
80
+ end
81
+
82
+ # Sanitizes the final document.
83
+ # @param [String] document html document
84
+ # @return [String] sanitized document
85
+ def postprocess(document)
86
+ HTML.sanitize.clean(@nav.render + document.force_encoding('utf-8'))
87
+ end
88
+
89
+ private
90
+
91
+ # Prepares an executable code block.
92
+ # @option opts [String] id author-supplied ID
93
+ # @option opts [String] raw code to execute
94
+ # @option opts [String] colored syntax highlighted code
95
+ # @return [String]
96
+ #def executable(opts)
97
+ # opts[:colored] + @exe.render(Object.new, id: opts[:id], raw: opts[:raw])
98
+ #end
99
+
100
+ # Prepares a problem form. Returns +yaml+ if the given text does not
101
+ # contain valid yaml markup for a problem.
102
+ # @param [String] yaml YAML markup
103
+ # @return [String] rendered HTML
104
+ def problem(yaml)
105
+ problem = Problem.parse(yaml)
106
+ Spirit.logger.record :problem, "ID: #{problem.id}"
107
+ problem.save! and problem.render(index: @prob += 1)
108
+ rescue RenderError
109
+ yaml
110
+ end
111
+
112
+ # Prepares a block image. Raises {RenderError} if the given text does not
113
+ # contain a valid image block.
114
+ # @param [String] text markdown text
115
+ # @return [String] rendered HTML
116
+ def block_image(text)
117
+ Image.parse(text).render(index: @img += 1)
118
+ end
119
+
120
+ # Wraps the given text with header tags.
121
+ # @return [String] rendered HTML
122
+ # @return [String] anchor name
123
+ def h(text, level)
124
+ header = @headers.add(text, level)
125
+ return header.render, header.name
126
+ end
127
+
128
+ # Wraps the given text with paragraph tags.
129
+ # @param [String] text paragraph text
130
+ # @return [String] rendered html
131
+ def p(text)
132
+ '<p>' + text + '</p>'
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+ end