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