spirit 0.1.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/lib/spirit.rb +22 -0
- data/lib/spirit/constants.rb +27 -0
- data/lib/spirit/document.rb +51 -0
- data/lib/spirit/errors.rb +12 -0
- data/lib/spirit/logger.rb +49 -0
- data/lib/spirit/manifest.rb +79 -0
- data/lib/spirit/render.rb +6 -0
- data/lib/spirit/render/errors.rb +11 -0
- data/lib/spirit/render/html.rb +138 -0
- data/lib/spirit/render/sanitize.rb +90 -0
- data/lib/spirit/render/templates.rb +6 -0
- data/lib/spirit/render/templates/header.rb +52 -0
- data/lib/spirit/render/templates/image.rb +54 -0
- data/lib/spirit/render/templates/multi.rb +40 -0
- data/lib/spirit/render/templates/navigation.rb +34 -0
- data/lib/spirit/render/templates/problem.rb +126 -0
- data/lib/spirit/render/templates/short.rb +30 -0
- data/lib/spirit/render/templates/table.rb +109 -0
- data/lib/spirit/render/templates/template.rb +32 -0
- data/lib/spirit/tilt/template.rb +35 -0
- data/lib/spirit/version.rb +4 -0
- data/views/exe.haml +5 -0
- data/views/header.haml +5 -0
- data/views/img.haml +6 -0
- data/views/multi.haml +16 -0
- data/views/nav.haml +5 -0
- data/views/short.haml +12 -0
- data/views/table.haml +28 -0
- metadata +256 -0
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.
|
data/README.md
ADDED
@@ -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
|
data/lib/spirit.rb
ADDED
@@ -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,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
|