spirit 0.1.0.pre

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/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