spirit 0.2 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/lib/spirit.rb +8 -10
  3. data/lib/spirit/constants.rb +17 -7
  4. data/lib/spirit/document.rb +4 -9
  5. data/lib/spirit/errors.rb +0 -1
  6. data/lib/spirit/logger.rb +5 -6
  7. data/lib/spirit/manifest.rb +4 -5
  8. data/lib/spirit/render.rb +0 -2
  9. data/lib/spirit/render/errors.rb +0 -4
  10. data/lib/spirit/render/html.rb +17 -117
  11. data/lib/spirit/render/processable.rb +78 -0
  12. data/lib/spirit/render/processors.rb +15 -0
  13. data/lib/spirit/render/processors/base.rb +40 -0
  14. data/lib/spirit/render/processors/block_image_processor.rb +49 -0
  15. data/lib/spirit/render/processors/headers_processor.rb +41 -0
  16. data/lib/spirit/render/processors/layout_processor.rb +28 -0
  17. data/lib/spirit/render/processors/math_processor.rb +102 -0
  18. data/lib/spirit/render/processors/problems_processor.rb +76 -0
  19. data/lib/spirit/render/processors/pygments_processor.rb +22 -0
  20. data/lib/spirit/render/processors/sanitize_processor.rb +86 -0
  21. data/lib/spirit/render/templates.rb +1 -3
  22. data/lib/spirit/render/templates/header.rb +2 -3
  23. data/lib/spirit/render/templates/image.rb +6 -13
  24. data/lib/spirit/render/templates/multi.rb +9 -10
  25. data/lib/spirit/render/templates/navigation.rb +4 -5
  26. data/lib/spirit/render/templates/problem.rb +24 -28
  27. data/lib/spirit/render/templates/short.rb +2 -3
  28. data/lib/spirit/render/templates/table.rb +2 -2
  29. data/lib/spirit/render/templates/template.rb +12 -8
  30. data/lib/spirit/version.rb +1 -2
  31. data/views/header.haml +1 -1
  32. data/views/img.haml +2 -2
  33. data/views/layout.haml +27 -0
  34. data/views/multi.haml +10 -14
  35. data/views/nav.haml +2 -2
  36. data/views/short.haml +6 -11
  37. data/views/table.haml +20 -26
  38. metadata +36 -57
  39. data/lib/spirit/render/sanitize.rb +0 -90
  40. 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(/&/, '&amp;')
85
+ .gsub(/</, '&lt;')
86
+ .gsub(/>/, '&gt;')
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(template header image problem multi short table navigation).each do |type|
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
- TEMPLATE = 'header.haml'
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
- TEMPLATE = 'img.haml'
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