spirit 0.2 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|