spirit 0.2 → 0.5
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.
- checksums.yaml +7 -0
- data/lib/spirit.rb +8 -10
- data/lib/spirit/constants.rb +17 -7
- data/lib/spirit/document.rb +4 -9
- data/lib/spirit/errors.rb +0 -1
- data/lib/spirit/logger.rb +5 -6
- data/lib/spirit/manifest.rb +4 -5
- data/lib/spirit/render.rb +0 -2
- data/lib/spirit/render/errors.rb +0 -4
- data/lib/spirit/render/html.rb +17 -117
- data/lib/spirit/render/processable.rb +78 -0
- data/lib/spirit/render/processors.rb +15 -0
- data/lib/spirit/render/processors/base.rb +40 -0
- data/lib/spirit/render/processors/block_image_processor.rb +49 -0
- data/lib/spirit/render/processors/headers_processor.rb +41 -0
- data/lib/spirit/render/processors/layout_processor.rb +28 -0
- data/lib/spirit/render/processors/math_processor.rb +102 -0
- data/lib/spirit/render/processors/problems_processor.rb +76 -0
- data/lib/spirit/render/processors/pygments_processor.rb +22 -0
- data/lib/spirit/render/processors/sanitize_processor.rb +86 -0
- data/lib/spirit/render/templates.rb +1 -3
- data/lib/spirit/render/templates/header.rb +2 -3
- data/lib/spirit/render/templates/image.rb +6 -13
- data/lib/spirit/render/templates/multi.rb +9 -10
- data/lib/spirit/render/templates/navigation.rb +4 -5
- data/lib/spirit/render/templates/problem.rb +24 -28
- data/lib/spirit/render/templates/short.rb +2 -3
- data/lib/spirit/render/templates/table.rb +2 -2
- data/lib/spirit/render/templates/template.rb +12 -8
- data/lib/spirit/version.rb +1 -2
- data/views/header.haml +1 -1
- data/views/img.haml +2 -2
- data/views/layout.haml +27 -0
- data/views/multi.haml +10 -14
- data/views/nav.haml +2 -2
- data/views/short.haml +6 -11
- data/views/table.haml +20 -26
- metadata +36 -57
- data/lib/spirit/render/sanitize.rb +0 -90
- data/views/exe.haml +0 -5
@@ -0,0 +1,49 @@
|
|
1
|
+
module Spirit
|
2
|
+
module Render
|
3
|
+
module Processors
|
4
|
+
|
5
|
+
class BlockImageProcessor < Base
|
6
|
+
|
7
|
+
process :paragraph, :filter
|
8
|
+
|
9
|
+
# Paragraphs that only contain images are rendered with
|
10
|
+
# {Spirit::Render::Image}.
|
11
|
+
IMAGE_REGEX = /\A\s*<img[^<>]+>\s*\z/m
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
@image = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
# Detects block images and renders them as such.
|
18
|
+
# @return [String] rendered html
|
19
|
+
def filter(text)
|
20
|
+
case text
|
21
|
+
when IMAGE_REGEX then block_image(text)
|
22
|
+
else p(text) end
|
23
|
+
rescue RenderError => e # fall back to paragraph
|
24
|
+
Spirit.logger.warn e.message
|
25
|
+
p(text)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Prepares a block image. Raises {RenderError} if the given text does
|
31
|
+
# not contain a valid image block.
|
32
|
+
# @param [String] text markdown text
|
33
|
+
# @return [String] rendered HTML
|
34
|
+
def block_image(text)
|
35
|
+
Image.parse(text).render(index: @image += 1)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Wraps the given text with paragraph tags.
|
39
|
+
# @param [String] text paragraph text
|
40
|
+
# @return [String] rendered html
|
41
|
+
def p(text)
|
42
|
+
'<p>' + text + '</p>'
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Spirit
|
2
|
+
module Render
|
3
|
+
module Processors
|
4
|
+
|
5
|
+
# In-charge of headers, navigation bar, and nesting.
|
6
|
+
# Depends on renderer#navigation and renderer#nesting
|
7
|
+
class HeadersProcessor < Base
|
8
|
+
|
9
|
+
process :header, :header
|
10
|
+
|
11
|
+
def initialize(renderer, *args)
|
12
|
+
renderer.nesting = @nesting = []
|
13
|
+
renderer.navigation = @navigation = Navigation.new
|
14
|
+
@headers = Headers.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Increases all header levels by one and keeps a navigation bar.
|
18
|
+
# @return [String] rendered html
|
19
|
+
def header(text, level)
|
20
|
+
h = headers.add(text, level += 1)
|
21
|
+
navigation.append(text, h.name) if level == 2
|
22
|
+
nest h
|
23
|
+
h.render
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_accessor :headers, :navigation, :nesting
|
29
|
+
|
30
|
+
# Maintains the +nesting+ array.
|
31
|
+
# @param [Header] h
|
32
|
+
def nest(h)
|
33
|
+
nesting.pop until nesting.empty? or h.level > nesting.last.level
|
34
|
+
nesting << h
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spirit
|
2
|
+
module Render
|
3
|
+
module Processors
|
4
|
+
|
5
|
+
# Post-processes a layout in HAML.
|
6
|
+
class LayoutProcessor < Base
|
7
|
+
|
8
|
+
TEMPLATE = File.join VIEWS, 'layout.haml'
|
9
|
+
|
10
|
+
attr_accessor :engine, :renderer
|
11
|
+
process :postprocess, :render
|
12
|
+
|
13
|
+
def initialize(renderer, *args)
|
14
|
+
template = File.read TEMPLATE
|
15
|
+
@engine = Haml::Engine.new template, HAML_CONFIG
|
16
|
+
@renderer = renderer
|
17
|
+
end
|
18
|
+
|
19
|
+
def render(document)
|
20
|
+
engine.render renderer,
|
21
|
+
content: document.force_encoding('utf-8')
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Spirit
|
2
|
+
module Render
|
3
|
+
module Processors
|
4
|
+
|
5
|
+
# Pre-processes math markup in latex.
|
6
|
+
# Adapted from
|
7
|
+
# http://www.math.union.edu/~dpvc/transfer/mathjax/mathjax-editing.js
|
8
|
+
class MathProcessor < Base
|
9
|
+
|
10
|
+
process :preprocess, :filter
|
11
|
+
process :postprocess, :replace
|
12
|
+
|
13
|
+
# Pattern for delimiters and special symbols; used to search for math in
|
14
|
+
# the document.
|
15
|
+
SPLIT = /(\$\$?| (?# 1 or 2 dollars)
|
16
|
+
\\(?:begin|end)\{[a-z]*\*?\}| (?# latex envs)
|
17
|
+
\\[\\{}$]| (?# \\ \{ \})
|
18
|
+
[{}]| (?# braces )
|
19
|
+
(?:\n\s*)+| (?# newlines w. optional spaces)
|
20
|
+
@@\d+@@) (?# @@digits@@)
|
21
|
+
/ix
|
22
|
+
|
23
|
+
def initialize(renderer, document)
|
24
|
+
reset_counters
|
25
|
+
@math = []
|
26
|
+
@blocks = document.gsub(/\r\n?/, "\n").split SPLIT
|
27
|
+
end
|
28
|
+
|
29
|
+
# Replace math in document with +@@index@@+.
|
30
|
+
# @return [String] document
|
31
|
+
def filter(document)
|
32
|
+
blocks.each_with_index do |block, i|
|
33
|
+
case
|
34
|
+
when '@' == block[0]
|
35
|
+
process_pseudo_marker block, i
|
36
|
+
when @start
|
37
|
+
process_potential_close block, i
|
38
|
+
else
|
39
|
+
process_potential_start block, i
|
40
|
+
end
|
41
|
+
end
|
42
|
+
process_math if @last
|
43
|
+
blocks.join
|
44
|
+
end
|
45
|
+
|
46
|
+
def replace(document)
|
47
|
+
document.gsub(/@@(\d+)@@/) { math[$1.to_i] }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :blocks, :math
|
53
|
+
|
54
|
+
def process_pseudo_marker(block, i)
|
55
|
+
blocks[i] = "@@#{math.length}@@"
|
56
|
+
math << block
|
57
|
+
end
|
58
|
+
|
59
|
+
def process_potential_start(block, i)
|
60
|
+
case block
|
61
|
+
when '$', '$$'
|
62
|
+
@start, @close, @braces = i, block, 0
|
63
|
+
when /\A\\begin\{([a-z]*\*?)\}/
|
64
|
+
@start, @close, @braces = i, "\\end{#{$1}}", 0
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_potential_close(block, i)
|
69
|
+
case block
|
70
|
+
when @close # process if braces match
|
71
|
+
@braces.zero? ? process_math(i) : @last = i
|
72
|
+
when /\n.*\n/ # don't go over double line breaks
|
73
|
+
process_math if @last
|
74
|
+
reset_counters
|
75
|
+
when '{' then @braces += 1 # balance braces
|
76
|
+
when '}' then @braces -= 1 if @braces > 0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Collects the math from blocks +i+ through +j+, replaces &, <, and > by
|
81
|
+
# named entities, and resets that math positions.
|
82
|
+
def process_math(last = @last)
|
83
|
+
block = blocks[@start..last].join
|
84
|
+
.gsub(/&/, '&')
|
85
|
+
.gsub(/</, '<')
|
86
|
+
.gsub(/>/, '>')
|
87
|
+
last.downto(@start+1) { |k| blocks[k] = '' }
|
88
|
+
blocks[@start] = "@@#{math.length}@@"
|
89
|
+
math << block
|
90
|
+
reset_counters
|
91
|
+
end
|
92
|
+
|
93
|
+
def reset_counters
|
94
|
+
@start = @close = @last = nil
|
95
|
+
@braces = 0
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Spirit
|
2
|
+
module Render
|
3
|
+
module Processors
|
4
|
+
|
5
|
+
# Pre-processes problem markup in YAML.
|
6
|
+
# Dependent on renderer#problems and renderer#nesting.
|
7
|
+
class ProblemsProcessor < Base
|
8
|
+
|
9
|
+
# Paragraphs that start and end with +"""+ are treated as embedded YAML
|
10
|
+
# and are parsed for questions/answers.
|
11
|
+
REGEX = /^"""$(.*?)^"""$/m
|
12
|
+
|
13
|
+
MARKER = /\A<!-- %%(\d+)%% -->\z/
|
14
|
+
|
15
|
+
attr_reader :problems, :solutions
|
16
|
+
delegate :size, :count, :each, :each_with_index, to: :problems
|
17
|
+
process :preprocess, :filter
|
18
|
+
process :block_html, :replace
|
19
|
+
|
20
|
+
def initialize(renderer, *args)
|
21
|
+
@renderer = renderer
|
22
|
+
@problems = []
|
23
|
+
@solutions = []
|
24
|
+
renderer.problems = self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Replaces YAML markup in document with <!-- %%index%% -->
|
28
|
+
# @return [String] document
|
29
|
+
def filter(document)
|
30
|
+
document.gsub(REGEX) { problem $1 }
|
31
|
+
end
|
32
|
+
|
33
|
+
def replace(html)
|
34
|
+
return html unless is_marker? html
|
35
|
+
replace_nesting html, renderer.nesting
|
36
|
+
''
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :renderer
|
42
|
+
|
43
|
+
# Update associated problem with nesting information.
|
44
|
+
# @return [void]
|
45
|
+
def replace_nesting(html, nesting)
|
46
|
+
match = html.strip.match MARKER
|
47
|
+
prob = problems[match[1].to_i]
|
48
|
+
prob.nesting = nesting.dup
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean] true iff the given html corresponds to a problem
|
52
|
+
# marker
|
53
|
+
def is_marker?(html)
|
54
|
+
html.strip =~ MARKER and problems[$1.to_i]
|
55
|
+
end
|
56
|
+
|
57
|
+
# If the given text contains valid YAML, returns a marker. Otherwise,
|
58
|
+
# returns the original text.
|
59
|
+
# @param [String] text candidate YAML markup
|
60
|
+
# @return [String] text or marker
|
61
|
+
def problem(text)
|
62
|
+
p = Problem.parse(text)
|
63
|
+
p.id = problems.size
|
64
|
+
self.problems << p
|
65
|
+
self.solutions << {digest: p.digest, solution: Marshal.dump(p.answer)}
|
66
|
+
Spirit.logger.record :problem, "ID: #{p.id}"
|
67
|
+
rescue RenderError
|
68
|
+
text
|
69
|
+
else "<!-- %%#{p.id}%% -->"
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'pygments'
|
2
|
+
module Spirit
|
3
|
+
module Render
|
4
|
+
module Processors
|
5
|
+
|
6
|
+
class PygmentsProcessor < Base
|
7
|
+
|
8
|
+
process :block_code, :highlight_code
|
9
|
+
|
10
|
+
# Pygmentizes code blocks.
|
11
|
+
# @param [String] code code block contents
|
12
|
+
# @param [String] marker name of language
|
13
|
+
# @return [String] highlighted code
|
14
|
+
def highlight_code(code, marker)
|
15
|
+
Pygments.highlight(code, lexer: marker || 'text')
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'sanitize'
|
2
|
+
|
3
|
+
module Spirit
|
4
|
+
module Render
|
5
|
+
module Processors
|
6
|
+
|
7
|
+
# Encapsulates sanitization options.
|
8
|
+
# @see https://github.com/github/gollum/blob/master/lib/gollum/sanitization.rb
|
9
|
+
class SanitizeProcessor < Base
|
10
|
+
|
11
|
+
process :postprocess, :clean
|
12
|
+
|
13
|
+
# white-listed elements
|
14
|
+
ELEMENTS = [
|
15
|
+
'a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
|
16
|
+
'blockquote', 'br', 'button', 'caption', 'center', 'cite',
|
17
|
+
'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir',
|
18
|
+
'div', 'dl', 'dt', 'em', 'fieldset', 'font', 'form', 'h1',
|
19
|
+
'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input',
|
20
|
+
'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu',
|
21
|
+
'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp',
|
22
|
+
'select', 'small', 'span', 'strike', 'strong', 'sub',
|
23
|
+
'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th',
|
24
|
+
'thead', 'tr', 'tt', 'u', 'ul', 'var'
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
# white-listed attributes
|
28
|
+
ATTRIBUTES = {
|
29
|
+
'a' => ['href', 'name', 'data-magellan-destination', 'data-action'],
|
30
|
+
'input' => ['data-max-page'],
|
31
|
+
'dd' => ['data-magellan-arrival'],
|
32
|
+
'dl' => ['data-magellan-expedition'],
|
33
|
+
'img' => ['src'],
|
34
|
+
:all => ['abbr', 'accept', 'accept-charset',
|
35
|
+
'accesskey', 'action', 'align', 'alt', 'axis',
|
36
|
+
'border', 'cellpadding', 'cellspacing', 'char',
|
37
|
+
'charoff', 'class', 'charset', 'checked', 'cite',
|
38
|
+
'clear', 'cols', 'colspan', 'color',
|
39
|
+
'compact', 'coords', 'datetime', 'dir',
|
40
|
+
'disabled', 'enctype', 'for', 'frame',
|
41
|
+
'headers', 'height', 'hreflang',
|
42
|
+
'hspace', 'id', 'ismap', 'label', 'lang',
|
43
|
+
'longdesc', 'maxlength', 'media', 'method',
|
44
|
+
'multiple', 'name', 'nohref', 'noshade',
|
45
|
+
'nowrap', 'prompt', 'readonly', 'rel', 'rev',
|
46
|
+
'rows', 'rowspan', 'rules', 'scope',
|
47
|
+
'selected', 'shape', 'size', 'span',
|
48
|
+
'start', 'summary', 'tabindex', 'target',
|
49
|
+
'title', 'type', 'usemap', 'valign', 'value',
|
50
|
+
'vspace', 'width']
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
# white-listed protocols
|
54
|
+
PROTOCOLS = {
|
55
|
+
'a' => {'href' => ['http', 'https', 'mailto', 'ftp', 'irc', 'apt', :relative]},
|
56
|
+
'img' => {'src' => ['http', 'https', :relative]}
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
# elements to remove (incl. contents)
|
60
|
+
REMOVE_CONTENTS = %w[script style].freeze
|
61
|
+
|
62
|
+
# attributes to add to elements
|
63
|
+
ADD_ATTRIBUTES = { 'a' => {'rel' => 'nofollow'} }.freeze
|
64
|
+
|
65
|
+
def self.config
|
66
|
+
{ elements: ELEMENTS.dup,
|
67
|
+
attributes: ATTRIBUTES.dup,
|
68
|
+
protocols: PROTOCOLS.dup,
|
69
|
+
add_attributes: ADD_ATTRIBUTES.dup,
|
70
|
+
remove_contents: REMOVE_CONTENTS.dup,
|
71
|
+
allow_comments: false
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
class_attribute :sanitizer
|
76
|
+
self.sanitizer = ::Sanitize.new config
|
77
|
+
|
78
|
+
def clean(document)
|
79
|
+
sanitizer.clean document
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
# ~*~ encoding: utf-8 ~*~
|
2
|
-
|
3
1
|
# require all template types
|
4
|
-
%w
|
2
|
+
%w[template header image problem multi short table navigation].each do |type|
|
5
3
|
require File.join 'spirit', 'render', 'templates', type
|
6
4
|
end
|
@@ -29,9 +29,8 @@ module Spirit
|
|
29
29
|
class Header < Template
|
30
30
|
|
31
31
|
# Name of template file for rendering headers.
|
32
|
-
|
33
|
-
|
34
|
-
attr_reader :name
|
32
|
+
self.template = 'header.haml'
|
33
|
+
attr_reader :name, :text, :level
|
35
34
|
|
36
35
|
# Creates a new header.
|
37
36
|
# @param [String] text header text
|
@@ -1,26 +1,20 @@
|
|
1
|
-
# ~*~ encoding: utf-8 ~*~
|
2
1
|
require 'nokogiri'
|
3
2
|
|
4
3
|
module Spirit
|
5
|
-
|
6
4
|
module Render
|
7
5
|
|
8
6
|
# Renders a block image with a figure number.
|
9
7
|
class Image < Template
|
10
8
|
|
11
9
|
# <img ...>
|
12
|
-
IMAGE_TAG = 'img'
|
10
|
+
IMAGE_TAG = 'img'.freeze
|
13
11
|
|
14
12
|
# Name of template file for rendering block images
|
15
|
-
|
16
|
-
|
17
|
-
class << self
|
18
|
-
|
19
|
-
# Parses the given text for a block image.
|
20
|
-
def parse(text)
|
21
|
-
Image.new text
|
22
|
-
end
|
13
|
+
self.template = 'img.haml'
|
23
14
|
|
15
|
+
# Parses the given text for a block image.
|
16
|
+
def self.parse(text)
|
17
|
+
Image.new text
|
24
18
|
end
|
25
19
|
|
26
20
|
# Creates a new image.
|
@@ -37,7 +31,7 @@ module Spirit
|
|
37
31
|
|
38
32
|
# Parses the given HTML, or raise {RenderError} if it is invalid.
|
39
33
|
def parse_or_raise
|
40
|
-
frag = Nokogiri::HTML::DocumentFragment.parse(@html)
|
34
|
+
frag = Nokogiri::HTML::DocumentFragment.parse(@html.strip)
|
41
35
|
if 1 == frag.children.count and
|
42
36
|
node = frag.children.first and
|
43
37
|
node.is_a? Nokogiri::XML::Element and
|
@@ -50,5 +44,4 @@ module Spirit
|
|
50
44
|
end
|
51
45
|
|
52
46
|
end
|
53
|
-
|
54
47
|
end
|