summaryrefslogtreecommitdiff
path: root/lib/banzai/renderer.rb
blob: 74663556cbb5fcf278f3d3e2b22e73d6adf1da98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
module Banzai
  module Renderer
    module_function

    # Convert a Markdown String into an HTML-safe String of HTML
    #
    # Note that while the returned HTML will have been sanitized of dangerous
    # HTML, it may post a risk of information leakage if it's not also passed
    # through `post_process`.
    #
    # Also note that the returned String is always HTML, not XHTML. Views
    # requiring XHTML, such as Atom feeds, need to call `post_process` on the
    # result, providing the appropriate `pipeline` option.
    #
    # text     - Markdown String
    # context  - Hash of context options passed to our HTML Pipeline
    #
    # Returns an HTML-safe String
    def render(text, context = {})
      cache_key = context.delete(:cache_key)
      cache_key = full_cache_key(cache_key, context[:pipeline])

      if cache_key
        Gitlab::Metrics.measure(:banzai_cached_render) do
          Rails.cache.fetch(cache_key) do
            cacheless_render(text, context)
          end
        end
      else
        cacheless_render(text, context)
      end
    end

    # Convert a Markdown-containing field on an object into an HTML-safe String
    # of HTML. This method is analogous to calling render(object.field), but it
    # can cache the rendered HTML in the object, rather than Redis.
    #
    # The context to use is learned from the passed-in object by calling
    # #banzai_render_context(field), and cannot be changed. Use #render, passing
    # it the field text, if a custom rendering is needed. The generated context
    # is returned along with the HTML.
    def render_field(object, field)
      html_field = object.markdown_cache_field_for(field)

      html = object.__send__(html_field)
      return html if html.present?

      html = cacheless_render_field(object, field)
      update_object(object, html_field, html) unless object.new_record? || object.destroyed?

      html
    end

    # Same as +render_field+, but without consulting or updating the cache field
    def cacheless_render_field(object, field, options = {})
      text = object.__send__(field)
      context = object.banzai_render_context(field).merge(options)

      cacheless_render(text, context)
    end

    # Perform multiple render from an Array of Markdown String into an
    # Array of HTML-safe String of HTML.
    #
    # As the rendered Markdown String can be already cached read all the data
    # from the cache using Rails.cache.read_multi operation. If the Markdown String
    # is not in the cache or it's not cacheable (no cache_key entry is provided in
    # the context) the Markdown String is rendered and stored in the cache so the
    # next render call gets the rendered HTML-safe String from the cache.
    #
    # For further explanation see #render method comments.
    #
    # texts_and_contexts - An Array of Hashes that contains the Markdown String (:text)
    #                      an options passed to our HTML Pipeline (:context)
    #
    # If on the :context you specify a :cache_key entry will be used to retrieve it
    # and cache the result of rendering the Markdown String.
    #
    # Returns an Array containing HTML-safe String instances.
    #
    # Example:
    #    texts_and_contexts
    #    => [{ text: '### Hello',
    #          context: { cache_key: [note, :note] } }]
    def cache_collection_render(texts_and_contexts)
      items_collection = texts_and_contexts.each_with_index do |item, index|
        context = item[:context]
        cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline])

        item[:cache_key] = cache_key if cache_key
      end

      cacheable_items, non_cacheable_items = items_collection.partition { |item| item.key?(:cache_key) }

      items_in_cache = []
      items_not_in_cache = []

      unless cacheable_items.empty?
        items_in_cache = Rails.cache.read_multi(*cacheable_items.map { |item| item[:cache_key] })
        items_not_in_cache = cacheable_items.reject do |item|
          item[:rendered] = items_in_cache[item[:cache_key]]
          items_in_cache.key?(item[:cache_key])
        end
      end

      (items_not_in_cache + non_cacheable_items).each do |item|
        item[:rendered] = render(item[:text], item[:context])
        Rails.cache.write(item[:cache_key], item[:rendered]) if item[:cache_key]
      end

      items_collection.map { |item| item[:rendered] }
    end

    def render_result(text, context = {})
      text = Pipeline[:pre_process].to_html(text, context) if text

      Pipeline[context[:pipeline]].call(text, context)
    end

    # Perform post-processing on an HTML String
    #
    # This method is used to perform state-dependent changes to a String of
    # HTML, such as removing references that the current user doesn't have
    # permission to make (`RedactorFilter`).
    #
    # html     - String to process
    # context  - Hash of options to customize output
    #            :pipeline  - Symbol pipeline type
    #            :project   - Project
    #            :user      - User object
    #
    # Returns an HTML-safe String
    def post_process(html, context)
      context = Pipeline[context[:pipeline]].transform_context(context)

      pipeline = Pipeline[:post_process]
      if context[:xhtml]
        pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
      else
        pipeline.to_html(html, context)
      end.html_safe
    end

    def cacheless_render(text, context = {})
      Gitlab::Metrics.measure(:banzai_cacheless_render) do
        result = render_result(text, context)

        output = result[:output]
        if output.respond_to?(:to_html)
          output.to_html
        else
          output.to_s
        end
      end
    end

    def full_cache_key(cache_key, pipeline_name)
      return unless cache_key
      ["banzai", *cache_key, pipeline_name || :full]
    end

    # To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key.
    # Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key
    # method.
    def full_cache_multi_key(cache_key, pipeline_name)
      return unless cache_key
      Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name))
    end

    def update_object(object, html_field, html)
      object.update_column(html_field, html)
    end
  end
end