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